tksbrokerapi.TKSBrokerAPI
TKSBrokerAPI is the trading platform for automation and simplifying the implementation of trading scenarios,
as well as working with Tinkoff Invest API server via the REST protocol. The TKSBrokerAPI platform may be used in two ways:
from the console, it has a rich keys and commands, or you can use it as Python module with python import.
TKSBrokerAPI allows you to automate routine trading operations and implement your trading scenarios, or just receive the necessary information from the broker. It is easy enough to integrate into various CI/CD automation systems.
- Open account for trading: https://tinkoff.ru/sl/AaX1Et1omnH
- TKSBrokerAPI module documentation: https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSBrokerAPI.html
- See CLI examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md#Usage-examples
- Used constants are in the TKSEnums module: https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSEnums.html
- About Tinkoff Invest API: https://tinkoff.github.io/investAPI/
- Tinkoff Invest API documentation: https://tinkoff.github.io/investAPI/swagger-ui/
1# -*- coding: utf-8 -*- 2# Author: Timur Gilmullin 3 4""" 5<a href="https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md"><img src="https://github.com/Tim55667757/TKSBrokerAPI/blob/develop/docs/media/TKSBrokerAPI-Logo.png?raw=true" alt="TKSBrokerAPI-Logo" width="780" target="_blank" /></a> 6 7**TKSBrokerAPI** is the trading platform for automation and simplifying the implementation of trading scenarios, 8as well as working with Tinkoff Invest API server via the REST protocol. The TKSBrokerAPI platform may be used in two ways: 9from the console, it has a rich keys and commands, or you can use it as Python module with `python import`. 10 11TKSBrokerAPI allows you to automate routine trading operations and implement your trading scenarios, or just receive 12the necessary information from the broker. It is easy enough to integrate into various CI/CD automation systems. 13 14- **Open account for trading:** https://tinkoff.ru/sl/AaX1Et1omnH 15- **TKSBrokerAPI module documentation:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSBrokerAPI.html 16- **See CLI examples:** https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md#Usage-examples 17- **Used constants are in the TKSEnums module:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSEnums.html 18- **About Tinkoff Invest API:** https://tinkoff.github.io/investAPI/ 19- **Tinkoff Invest API documentation:** https://tinkoff.github.io/investAPI/swagger-ui/ 20""" 21 22# Copyright (c) 2022 Gilmillin Timur Mansurovich 23# 24# Licensed under the Apache License, Version 2.0 (the "License"); 25# you may not use this file except in compliance with the License. 26# You may obtain a copy of the License at 27# 28# http://www.apache.org/licenses/LICENSE-2.0 29# 30# Unless required by applicable law or agreed to in writing, software 31# distributed under the License is distributed on an "AS IS" BASIS, 32# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 33# See the License for the specific language governing permissions and 34# limitations under the License. 35 36 37import sys 38import os 39from argparse import ArgumentParser 40from importlib.metadata import version 41 42from dateutil.tz import tzlocal 43from time import sleep 44 45import re 46import json 47import requests 48import traceback as tb 49from typing import Union 50 51from multiprocessing import cpu_count, Lock 52from multiprocessing.pool import ThreadPool 53import pandas as pd 54 55from mako.template import Template # Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance. 56from Templates import * # Some html-templates used by reporting methods in TKSBrokerAPI module 57from TKSEnums import * # A lot of constants from enums sections: https://tinkoff.github.io/investAPI/swagger-ui/ 58from TradeRoutines import * # This library contains some methods used by trade scenarios implemented with TKSBrokerAPI module 59 60from pricegenerator.PriceGenerator import PriceGenerator, uLogger # This module has a lot of instruments to work with candles data (https://github.com/Tim55667757/PriceGenerator) 61from pricegenerator.UniLogger import DisableLogger as PGDisLog # Method for disable log from PriceGenerator 62 63import UniLogger as uLog # Logger for TKSBrokerAPI 64 65 66# --- Common technical parameters: 67 68PGDisLog(uLogger.handlers[0]) # Disable 3-rd party logging from PriceGenerator 69uLogger = uLog.UniLogger # init logger for TKSBrokerAPI 70uLogger.level = 10 # debug level by default for TKSBrokerAPI module 71uLogger.handlers[0].level = 20 # info level by default for STDOUT of TKSBrokerAPI module 72 73__version__ = "1.6" # The "major.minor" version setup here, but build number define at the build-server only 74 75CPU_COUNT = cpu_count() # host's real CPU count 76CPU_USAGES = CPU_COUNT - 1 if CPU_COUNT > 1 else 1 # how many CPUs will be used for parallel calculations 77 78 79class TinkoffBrokerServer: 80 """ 81 This class implements methods to work with Tinkoff broker server. 82 83 Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/ 84 85 About `token`: https://tinkoff.github.io/investAPI/token/ 86 """ 87 def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None: 88 """ 89 Main class init. 90 91 :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`. 92 :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. 93 Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`. 94 :param useCache: use default cache file with raw data to use instead of `iList`. 95 True by default. Cache is auto-update if new day has come. 96 If you don't want to use cache and always updates raw data then set `useCache=False`. 97 :param defaultCache: path to default cache file. `dump.json` by default. 98 """ 99 if token is None or not token: 100 try: 101 self.token = r"{}".format(os.environ["TKS_API_TOKEN"]) 102 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/") 103 104 except KeyError: 105 uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/") 106 raise Exception("Token required") 107 108 else: 109 self.token = token # highly priority than environment variable 'TKS_API_TOKEN' 110 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`") 111 112 if accountId is None or not accountId: 113 try: 114 self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"]) 115 uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId)) 116 117 except KeyError: 118 uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).") 119 120 else: 121 self.accountId = accountId # highly priority than environment variable 'TKS_ACCOUNT_ID' 122 uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId)) 123 124 self.version = __version__ # duplicate here used TKSBrokerAPI main version 125 """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only. 126 127 Latest version: https://pypi.org/project/tksbrokerapi/ 128 """ 129 130 self._tag = "" 131 """Identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode. Default: `""` (empty string).""" 132 133 self.__lock = Lock() # initialize multiprocessing mutex lock 134 135 self._precision = 4 # precision, signs after comma, e.g. 2 for instruments like PLZL, 4 for instruments like USDRUB, if -1 then auto detect it when load data-file 136 137 self.aliases = TKS_TICKER_ALIASES 138 """Some aliases instead official tickers. 139 140 See also: `TKSEnums.TKS_TICKER_ALIASES` 141 """ 142 143 self.aliasesKeys = self.aliases.keys() # re-calc only first time at class init 144 145 self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there 146 147 self._ticker = "" 148 """String with ticker, e.g. `GOOGL`. Tickers may be upper case only. 149 150 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 151 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 152 153 See also: `SearchByTicker()`, `SearchInstruments()`. 154 """ 155 156 self._figi = "" 157 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 158 159 See also: `SearchByFIGI()`, `SearchInstruments()`. 160 """ 161 162 self.depth = 1 163 """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI. 164 165 See also: `GetCurrentPrices()`. 166 """ 167 168 self.server = r"https://invest-public-api.tinkoff.ru/rest" 169 """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest 170 171 See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`. 172 """ 173 174 uLogger.debug("Broker API server: {}".format(self.server)) 175 176 self.timeout = 15 177 """Server operations timeout in seconds. Default: `15`. 178 179 See also: `SendAPIRequest()`. 180 """ 181 182 self.headers = { 183 "Content-Type": "application/json", 184 "accept": "application/json", 185 "Authorization": "Bearer {}".format(self.token), 186 "x-app-name": "Tim55667757.TKSBrokerAPI", 187 } 188 """ 189 Headers which send in every request to broker server. Please, do not change it! 190 Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}", "x-app-name": "Tim55667757.TKSBrokerAPI"}`. 191 192 See also: `SendAPIRequest()`. 193 """ 194 195 self.body = None 196 """Request body which send to broker server. Default: `None`. 197 198 See also: `SendAPIRequest()`. 199 """ 200 201 self.moreDebug = False 202 """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default.""" 203 204 self.useHTMLReports = False 205 """ 206 If `True` then TKSBrokerAPI generate also HTML reports from Markdown. `False` by default. 207 208 See also: Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance. 209 """ 210 211 self.historyFile = None 212 """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame. 213 214 See also: `History()`. 215 """ 216 217 self.htmlHistoryFile = "index.html" 218 """Full path to the html file where rendered candles chart stored. Default: `index.html`. 219 220 See also: `ShowHistoryChart()`. 221 """ 222 223 self.instrumentsFile = "instruments.md" 224 """Filename where full available to user instruments list will be saved. Default: `instruments.md`. 225 226 See also: `ShowInstrumentsInfo()`. 227 """ 228 229 self.searchResultsFile = "search-results.md" 230 """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`. 231 232 See also: `SearchInstruments()`. 233 """ 234 235 self.pricesFile = "prices.md" 236 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 237 238 See also: `GetListOfPrices()`. 239 """ 240 241 self.infoFile = "info.md" 242 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 243 244 See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`. 245 """ 246 247 self.bondsXLSXFile = "ext-bonds.xlsx" 248 """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 249 bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`. 250 251 See also: `ExtendBondsData()`. 252 """ 253 254 self.calendarFile = "calendar.md" 255 """Filename where bonds payment calendar will be saved. Default: `calendar.md`. 256 257 Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`. 258 259 See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`. 260 """ 261 262 self.overviewFile = "overview.md" 263 """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`. 264 265 See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`. 266 """ 267 268 self.overviewDigestFile = "overview-digest.md" 269 """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`. 270 271 See also: `Overview()` with parameter `details="digest"`. 272 """ 273 274 self.overviewPositionsFile = "overview-positions.md" 275 """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`. 276 277 See also: `Overview()` with parameter `details="positions"`. 278 """ 279 280 self.overviewOrdersFile = "overview-orders.md" 281 """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`. 282 283 See also: `Overview()` with parameter `details="orders"`. 284 """ 285 286 self.overviewAnalyticsFile = "overview-analytics.md" 287 """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`. 288 289 See also: `Overview()` with parameter `details="analytics"`. 290 """ 291 292 self.overviewBondsCalendarFile = "overview-calendar.md" 293 """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`. 294 295 See also: `Overview()` with parameter `details="calendar"`. 296 """ 297 298 self.reportFile = "deals.md" 299 """Filename where history of deals and trade statistics will be saved. Default: `deals.md`. 300 301 See also: `Deals()`. 302 """ 303 304 self.withdrawalLimitsFile = "limits.md" 305 """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`. 306 307 See also: `OverviewLimits()` and `RequestLimits()`. 308 """ 309 310 self.userInfoFile = "user-info.md" 311 """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`. 312 313 See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`. 314 """ 315 316 self.userAccountsFile = "accounts.md" 317 """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`. 318 319 See also: `OverviewAccounts()`, `RequestAccounts()`. 320 """ 321 322 self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache 323 """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`. 324 325 Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`. 326 327 See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`. 328 """ 329 330 self.iList = None # init iList for raw instruments data 331 """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`. 332 333 See also: `Listing()`, `DumpInstruments()`. 334 """ 335 336 # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server: 337 if useCache: 338 if os.path.exists(self.iListDumpFile): 339 dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc()) # dump modification date and time 340 curTime = datetime.now(tzutc()) 341 342 if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year): 343 uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 344 345 self.DumpInstruments(forceUpdate=True) # updating self.iList and dump file 346 347 else: 348 self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8")) # load iList from dump 349 350 uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format( 351 os.path.abspath(self.iListDumpFile), 352 dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT), 353 )) 354 355 else: 356 uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...") 357 self.DumpInstruments(forceUpdate=True) # updating self.iList and creating default dump file 358 359 else: 360 self.iList = self.Listing() # request new raw instruments data from broker server 361 self.DumpInstruments(forceUpdate=False) # save raw instrument's data to default dump file `iListDumpFile` 362 363 self.priceModel = PriceGenerator() # init PriceGenerator object to work with candles data 364 """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on. 365 366 See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator 367 """ 368 369 @property 370 def tag(self) -> str: 371 """Identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode. Default: `""` (empty string).""" 372 return self._tag 373 374 @tag.setter 375 def tag(self, value): 376 """Setter for Identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode. Default: `""` (empty string).""" 377 self._tag = str(value) 378 379 if self._tag: 380 for handler in uLogger.handlers: 381 handler.setFormatter(uLog.logging.Formatter(uLog.formatStringWithTag.format(tag=self._tag))) 382 383 uLogger.debug("Custom TKSBrokerAPI tag was set: {}".format(self._tag)) 384 385 else: 386 for handler in uLogger.handlers: 387 handler.setFormatter(uLog.logging.Formatter(uLog.formatString)) 388 389 uLogger.debug("Default logger format is used") 390 391 @property 392 def ticker(self) -> str: 393 """String with ticker, e.g. `GOOGL`. Tickers may be upper case only. 394 395 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 396 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 397 398 See also: `SearchByTicker()`, `SearchInstruments()`. 399 """ 400 return self._ticker 401 402 @ticker.setter 403 def ticker(self, value): 404 """Setter for string with ticker, e.g. `GOOGL`. Tickers may be upper case only. 405 406 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 407 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 408 409 See also: `SearchByTicker()`, `SearchInstruments()`. 410 """ 411 self._ticker = str(value).upper() # Tickers may be upper case only 412 413 @property 414 def figi(self) -> str: 415 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 416 417 See also: `SearchByFIGI()`, `SearchInstruments()`. 418 """ 419 return self._figi 420 421 @figi.setter 422 def figi(self, value): 423 """Setter for string with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 424 425 See also: `SearchByFIGI()`, `SearchInstruments()`. 426 """ 427 self._figi = str(value).upper() # FIGI may be upper case only 428 429 @property 430 def precision(self) -> int: 431 return self._precision 432 433 @precision.setter 434 def precision(self, value): 435 if value >= 0: 436 self._precision = value 437 438 else: 439 self._precision = -1 # auto-detect precision next when data-file load 440 441 def _ParseJSON(self, rawData="{}") -> dict: 442 """ 443 Parse JSON from response string. 444 445 :param rawData: this is a string with JSON-formatted text. 446 :return: JSON (dictionary), parsed from server response string. If an error occurred, then returns empty dict `{}`. 447 """ 448 try: 449 responseJSON = json.loads(rawData) if rawData else {} 450 451 if self.moreDebug: 452 uLogger.debug("JSON formatted raw body data of response:\n{}".format(json.dumps(responseJSON, indent=4))) 453 454 return responseJSON 455 456 except Exception as e: 457 uLogger.error("An empty dict will be return, because an error occurred in `_ParseJSON()` method with comment: {}".format(e)) 458 459 return {} 460 461 def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict: 462 """ 463 Send GET or POST request to broker server and receive JSON object. 464 465 self.header: must be defining with dictionary of headers. 466 self.body: if define then used as request body. None by default. 467 self.timeout: global request timeout, 15 seconds by default. 468 :param url: url with REST request. 469 :param reqType: send "GET" or "POST" request. "GET" by default. 470 :param retry: how many times retry after first request if an 5xx server errors occurred. 471 :param pause: sleep time in seconds between retries. 472 :return: response JSON (dictionary) from broker. 473 """ 474 if reqType.upper() not in ("GET", "POST"): 475 uLogger.error("You can define request type: `GET` or `POST`!") 476 raise Exception("Incorrect value") 477 478 if self.moreDebug: 479 uLogger.debug("Request parameters:") 480 uLogger.debug(" - REST API URL: {}".format(url)) 481 uLogger.debug(" - request type: {}".format(reqType)) 482 uLogger.debug(" - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***"))) 483 uLogger.debug(" - body:\n{}".format(self.body)) 484 485 # fast hack to avoid all operations with some tickers/FIGI 486 responseJSON = {} 487 oK = True 488 for item in self.exclude: 489 if item in url: 490 if self.moreDebug: 491 uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude))) 492 493 oK = False 494 break 495 496 if oK: 497 with self.__lock: # acquire the mutex lock 498 counter = 0 499 response = None 500 errMsg = "" 501 502 while not response and counter <= retry: 503 if reqType == "GET": 504 response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout) 505 506 if reqType == "POST": 507 response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout) 508 509 if self.moreDebug: 510 uLogger.debug("Response:") 511 uLogger.debug(" - status code: {}".format(response.status_code)) 512 uLogger.debug(" - reason: {}".format(response.reason)) 513 uLogger.debug(" - body length: {}".format(len(response.text))) 514 uLogger.debug(" - headers:\n{}".format(response.headers)) 515 516 # Server returns some headers: 517 # - `x-ratelimit-limit` — shows the settings of the current user limit for this method. 518 # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute. 519 # - `x-ratelimit-reset` — time in seconds before resetting the request counter. 520 # See: https://tinkoff.github.io/investAPI/grpc/#kreya 521 if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0": 522 rateLimitWait = int(response.headers["x-ratelimit-reset"]) 523 uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait)) 524 sleep(rateLimitWait) 525 526 # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes 527 if 400 <= response.status_code < 500: 528 msg = "status code: [{}], response body: {}".format(response.status_code, response.text) 529 uLogger.debug(" - not oK, but do not retry for 4xx errors, {}".format(msg)) 530 531 if "code" in response.text and "message" in response.text: 532 msgDict = self._ParseJSON(rawData=response.text) 533 uLogger.warning("HTTP-status code [{}], server message: {}".format(response.status_code, msgDict["message"])) 534 535 counter = retry + 1 # do not retry for 4xx errors 536 537 if 500 <= response.status_code < 600: 538 errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text) 539 uLogger.debug(" - not oK, {}".format(errMsg)) 540 541 if "code" in response.text and "message" in response.text: 542 errMsgDict = self._ParseJSON(rawData=response.text) 543 uLogger.warning("HTTP-status code [{}], error message: {}".format(response.status_code, errMsgDict["message"])) 544 545 counter += 1 546 547 if counter <= retry: 548 uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause)) 549 sleep(pause) 550 551 responseJSON = self._ParseJSON(rawData=response.text) 552 553 if errMsg: 554 uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/") 555 uLogger.error(" - not oK, {}".format(errMsg)) 556 557 return responseJSON 558 559 def _IUpdater(self, iType: str) -> tuple: 560 """ 561 Request instrument by type from server. See available API methods for instruments: 562 Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies 563 Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares 564 Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds 565 Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs 566 Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures 567 568 :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list. 569 :return: tuple with iType name and list of available instruments of current type for defined user token. 570 """ 571 result = [] 572 573 if iType in TKS_INSTRUMENTS: 574 uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType)) 575 576 # all instruments have the same body in API v2 requests: 577 self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"}) # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL] 578 instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType) 579 result = self.SendAPIRequest(instrumentURL, reqType="POST")["instruments"] 580 581 return iType, result 582 583 def _IWrapper(self, kwargs): 584 """ 585 Wrapper runs instrument's update method `_IUpdater()`. 586 It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206 587 """ 588 return self._IUpdater(**kwargs) 589 590 def Listing(self) -> dict: 591 """ 592 Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server. 593 594 :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures. 595 """ 596 uLogger.debug("Requesting all available instruments for current account. Wait, please...") 597 uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES)) 598 599 # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService 600 # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list. 601 iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS] 602 603 poolUpdater = ThreadPool(processes=CPU_USAGES) # create pool for update instruments in parallel mode 604 listing = poolUpdater.map(self._IWrapper, iParams) # execute update operations 605 poolUpdater.close() # close the thread pool 606 poolUpdater.join() # wait a moment until all data returns from threads 607 608 # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures. 609 # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method 610 iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing} 611 612 # calculate minimum price increment (step) for all instruments and set up instrument's type: 613 for iType in iList.keys(): 614 for ticker in iList[iType]: 615 iList[iType][ticker]["type"] = iType 616 617 if "minPriceIncrement" in iList[iType][ticker].keys(): 618 iList[iType][ticker]["step"] = NanoToFloat( 619 iList[iType][ticker]["minPriceIncrement"]["units"], 620 iList[iType][ticker]["minPriceIncrement"]["nano"], 621 ) 622 623 else: 624 iList[iType][ticker]["step"] = 0 # hack to avoid empty value in some instruments, e.g. futures 625 626 return iList 627 628 def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None: 629 """ 630 Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics. 631 632 See also: `DumpInstruments()`, `Listing()`. 633 634 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 635 otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) . 636 """ 637 if self.iListDumpFile is None or not self.iListDumpFile: 638 uLogger.error("Output name of dump file must be defined!") 639 raise Exception("Filename required") 640 641 if not self.iList or forceUpdate: 642 self.iList = self.Listing() 643 644 xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx" 645 646 # Save as XLSX with separated sheets for every type of instruments: 647 with pd.ExcelWriter( 648 path=xlsxDumpFile, 649 date_format=TKS_DATE_FORMAT, 650 datetime_format=TKS_DATE_TIME_FORMAT, 651 mode="w", 652 ) as writer: 653 for iType in TKS_INSTRUMENTS: 654 df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index") # generate pandas object from self.iList dictionary 655 df = df[sorted(df)] # sorted by column names 656 df = df.applymap( 657 lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item, 658 na_action="ignore", 659 ) # converting numbers from nano-type to float in every cell 660 df.to_excel( 661 writer, 662 sheet_name=iType, 663 encoding="UTF-8", 664 freeze_panes=(1, 1), 665 ) # saving as XLSX-file with freeze first row and column as headers 666 667 uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile))) 668 669 def DumpInstruments(self, forceUpdate: bool = True) -> str: 670 """ 671 Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server 672 using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file. 673 674 See also: `DumpInstrumentsAsXLSX()`, `Listing()`. 675 676 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 677 otherwise just saves exist `iList` as JSON-file (default: `dump.json`). 678 :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file. 679 """ 680 if self.iListDumpFile is None or not self.iListDumpFile: 681 uLogger.error("Output name of dump file must be defined!") 682 raise Exception("Filename required") 683 684 if not self.iList or forceUpdate: 685 self.iList = self.Listing() 686 687 jsonDump = json.dumps(self.iList, indent=4, sort_keys=False) # create JSON object as string 688 with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH: 689 fH.write(jsonDump) 690 691 uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile))) 692 693 return jsonDump 694 695 def ShowInstrumentInfo(self, iJSON: dict, show: bool = True, onlyFiles=False) -> str: 696 """ 697 Show information about one instrument defined by json data and prints it in Markdown format. 698 699 See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`. 700 701 :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self._ticker]` 702 :param show: if `True` then also printing information about instrument and its current price. 703 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 704 :return: multilines text in Markdown format with information about one instrument. 705 """ 706 splitLine = "| | |\n" 707 infoText = "" 708 709 if iJSON is not None and iJSON and isinstance(iJSON, dict): 710 info = [ 711 "# Main information\n\n", 712 "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 713 "| Parameters | Values |\n", 714 "|-------------------------------------------------------------|--------------------------------------------------------|\n", 715 "| Ticker: | {:<54} |\n".format(iJSON["ticker"]), 716 "| Full name: | {:<54} |\n".format(iJSON["name"]), 717 ] 718 719 if "sector" in iJSON.keys() and iJSON["sector"]: 720 info.append("| Sector: | {:<54} |\n".format(iJSON["sector"])) 721 722 if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]: 723 info.append("| Country of instrument: | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"]))) 724 725 info.extend([ 726 splitLine, 727 "| FIGI (Financial Instrument Global Identifier): | {:<54} |\n".format(iJSON["figi"]), 728 "| Real exchange [Exchange section]: | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])), 729 ]) 730 731 if "isin" in iJSON.keys() and iJSON["isin"]: 732 info.append("| ISIN (International Securities Identification Number): | {:<54} |\n".format(iJSON["isin"])) 733 734 if "classCode" in iJSON.keys(): 735 info.append("| Class Code (exchange section where instrument is traded): | {:<54} |\n".format(iJSON["classCode"])) 736 737 info.extend([ 738 splitLine, 739 "| Current broker security trading status: | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]), 740 splitLine, 741 "| Buy operations allowed: | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"), 742 "| Sale operations allowed: | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"), 743 "| Short positions allowed: | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"), 744 ]) 745 746 if iJSON["figi"]: 747 self._figi = iJSON["figi"] 748 iJSON = iJSON | self.RequestTradingStatus() 749 750 info.extend([ 751 splitLine, 752 "| Limit orders allowed: | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"), 753 "| Market orders allowed: | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"), 754 "| API trade allowed: | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"), 755 ]) 756 757 info.append(splitLine) 758 759 if "type" in iJSON.keys() and iJSON["type"]: 760 info.append("| Type of the instrument: | {:<54} |\n".format(iJSON["type"])) 761 762 if "shareType" in iJSON.keys() and iJSON["shareType"]: 763 info.append("| Share type: | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]])) 764 765 if "futuresType" in iJSON.keys() and iJSON["futuresType"]: 766 info.append("| Futures type: | {:<54} |\n".format(iJSON["futuresType"])) 767 768 if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]: 769 info.append("| IPO date: | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", ""))) 770 771 if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]: 772 info.append("| Released date: | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", ""))) 773 774 if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]: 775 info.append("| Rebalancing frequency: | {:<54} |\n".format(iJSON["rebalancingFreq"])) 776 777 if "focusType" in iJSON.keys() and iJSON["focusType"]: 778 info.append("| Focusing type: | {:<54} |\n".format(iJSON["focusType"])) 779 780 if "assetType" in iJSON.keys() and iJSON["assetType"]: 781 info.append("| Asset type: | {:<54} |\n".format(iJSON["assetType"])) 782 783 if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]: 784 info.append("| Basic asset: | {:<54} |\n".format(iJSON["basicAsset"])) 785 786 if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]: 787 info.append("| Basic asset size: | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"])))) 788 789 if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]: 790 info.append("| ISO currency name: | {:<54} |\n".format(iJSON["isoCurrencyName"])) 791 792 if "currency" in iJSON.keys(): 793 info.append("| Payment currency: | {:<54} |\n".format(iJSON["currency"])) 794 795 if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys(): 796 info.append("| Nominal currency: | {:<54} |\n".format(iJSON["nominal"]["currency"])) 797 798 if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]: 799 info.append("| First trade date: | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", ""))) 800 801 if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]: 802 info.append("| Last trade date: | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", ""))) 803 804 if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]: 805 info.append("| Date of expiration: | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", ""))) 806 807 if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]: 808 info.append("| State registration date: | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", ""))) 809 810 if "placementDate" in iJSON.keys() and iJSON["placementDate"]: 811 info.append("| Placement date: | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", ""))) 812 813 if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]: 814 info.append("| Maturity date: | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", ""))) 815 816 if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]: 817 info.append("| Perpetual bond: | Yes |\n") 818 819 if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]: 820 info.append("| Over-the-counter (OTC) securities: | Yes |\n") 821 822 iExt = None 823 if iJSON["type"] == "Bonds": 824 info.extend([ 825 splitLine, 826 "| Bond issue (size / plan): | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])), 827 "| Nominal price (100%): | {:<54} |\n".format("{} {}".format( 828 "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."), 829 iJSON["nominal"]["currency"], 830 )), 831 ]) 832 833 if "floatingCouponFlag" in iJSON.keys(): 834 info.append("| Floating coupon: | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No")) 835 836 if "amortizationFlag" in iJSON.keys(): 837 info.append("| Amortization: | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No")) 838 839 info.append(splitLine) 840 841 if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]: 842 info.append("| Number of coupon payments per year: | {:<54} |\n".format(iJSON["couponQuantityPerYear"])) 843 844 if iJSON["figi"]: 845 iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False) # extended bonds data 846 847 info.extend([ 848 "| Days last to maturity date: | {:<54} |\n".format(iExt["daysToMaturity"][0]), 849 "| Coupons yield (average coupon daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])), 850 "| Current price yield (average daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])), 851 ]) 852 853 if "aciValue" in iJSON.keys() and iJSON["aciValue"]: 854 info.append("| Current accumulated coupon income (ACI): | {:<54} |\n".format("{:.2f} {}".format( 855 NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]), 856 iJSON["aciValue"]["currency"] 857 ))) 858 859 if "currentPrice" in iJSON.keys(): 860 info.append(splitLine) 861 862 currency = iJSON["currency"] if "currency" in iJSON.keys() else "" # nominal currency for bonds, otherwise currency of instrument 863 aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else "" # payment currency 864 865 bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0 # previous close price of bond 866 bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0 # last price of bond 867 bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0 # max price of bond 868 bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0 # min price of bond 869 bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0 # delta between last deal price and last close 870 871 curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0 872 curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0 873 874 info.extend([ 875 "| Previous close price of the instrument: | {:<54} |\n".format("{}{}".format( 876 "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A", 877 "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 878 )), 879 "| Last deal price of the instrument: | {:<54} |\n".format("{}{}".format( 880 "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A", 881 "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 882 )), 883 "| Changes between last deal price and last close | {:<54} |\n".format( 884 "{:.2f}%{}".format( 885 iJSON["currentPrice"]["changes"], 886 " ({}{:.2f} {})".format( 887 "+" if bondChangesDelta > 0 else "", 888 bondChangesDelta, 889 aciCurrency 890 ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format( 891 "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "", 892 iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"], 893 currency 894 ), 895 ) 896 ), 897 "| Current limit price, min / max: | {:<54} |\n".format("{}{} / {}{}{}".format( 898 "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A", 899 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 900 "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A", 901 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 902 " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else "" 903 )), 904 "| Actual price, sell / buy: | {:<54} |\n".format("{}{} / {}{}{}".format( 905 "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A", 906 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 907 "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A", 908 "%" if iJSON["type"] == "Bonds" else" {}".format(currency), 909 " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else "" 910 )), 911 ]) 912 913 if "lot" in iJSON.keys(): 914 info.append("| Minimum lot to buy: | {:<54} |\n".format(iJSON["lot"])) 915 916 if "step" in iJSON.keys() and iJSON["step"] != 0: 917 info.append("| Minimum price increment (step): | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else ""))) 918 919 # Add bond payment calendar: 920 if iJSON["type"] == "Bonds": 921 strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False) # bond payment calendar 922 info.extend(["\n#", strCalendar]) 923 924 infoText += "".join(info) 925 926 if show and not onlyFiles: 927 uLogger.info("{}".format(infoText)) 928 929 if self.infoFile is not None and (show or onlyFiles): 930 with open(self.infoFile, "w", encoding="UTF-8") as fH: 931 fH.write(infoText) 932 933 uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile))) 934 935 if self.useHTMLReports: 936 htmlFilePath = self.infoFile.replace(".md", ".html") if self.infoFile.endswith(".md") else self.infoFile + ".html" 937 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 938 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Main information", commonCSS=COMMON_CSS, markdown=infoText)) 939 940 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 941 942 return infoText 943 944 def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict: 945 """ 946 Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined! 947 948 :param requestPrice: if `False` then do not request current price of instrument (because this is long operation). 949 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 950 :return: JSON formatted data with information about instrument. 951 """ 952 tickerJSON = {} 953 if self.moreDebug: 954 uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self._ticker)) 955 956 if not self._ticker: 957 uLogger.warning("self._ticker variable is not be empty!") 958 959 else: 960 if self._ticker in TKS_TICKERS_OR_FIGI_EXCLUDED: 961 uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self._ticker)) 962 raise Exception("Instrument not allowed") 963 964 if not self.iList: 965 self.iList = self.Listing() 966 967 if self._ticker in self.iList["Shares"].keys(): 968 tickerJSON = self.iList["Shares"][self._ticker] 969 if self.moreDebug: 970 uLogger.debug("Ticker [{}] found in shares list".format(self._ticker)) 971 972 elif self._ticker in self.iList["Currencies"].keys(): 973 tickerJSON = self.iList["Currencies"][self._ticker] 974 if self.moreDebug: 975 uLogger.debug("Ticker [{}] found in currencies list".format(self._ticker)) 976 977 elif self._ticker in self.iList["Bonds"].keys(): 978 tickerJSON = self.iList["Bonds"][self._ticker] 979 if self.moreDebug: 980 uLogger.debug("Ticker [{}] found in bonds list".format(self._ticker)) 981 982 elif self._ticker in self.iList["Etfs"].keys(): 983 tickerJSON = self.iList["Etfs"][self._ticker] 984 if self.moreDebug: 985 uLogger.debug("Ticker [{}] found in etfs list".format(self._ticker)) 986 987 elif self._ticker in self.iList["Futures"].keys(): 988 tickerJSON = self.iList["Futures"][self._ticker] 989 if self.moreDebug: 990 uLogger.debug("Ticker [{}] found in futures list".format(self._ticker)) 991 992 if tickerJSON: 993 self._figi = tickerJSON["figi"] 994 995 if requestPrice: 996 tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False) 997 998 if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None: 999 tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"] 1000 1001 else: 1002 tickerJSON["currentPrice"]["changes"] = 0 1003 1004 if show: 1005 self.ShowInstrumentInfo(iJSON=tickerJSON, show=True) # print info as Markdown text 1006 1007 else: 1008 if show: 1009 uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self._ticker)) 1010 1011 return tickerJSON 1012 1013 def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict: 1014 """ 1015 Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined! 1016 1017 :param requestPrice: if `False` then do not request current price of instrument (it's long operation). 1018 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 1019 :return: JSON formatted data with information about instrument. 1020 """ 1021 figiJSON = {} 1022 if self.moreDebug: 1023 uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self._figi)) 1024 1025 if not self._figi: 1026 uLogger.warning("self._figi variable is not be empty!") 1027 1028 else: 1029 if self._figi in TKS_TICKERS_OR_FIGI_EXCLUDED: 1030 uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self._figi)) 1031 raise Exception("Instrument not allowed") 1032 1033 if not self.iList: 1034 self.iList = self.Listing() 1035 1036 for item in self.iList["Shares"].keys(): 1037 if self._figi == self.iList["Shares"][item]["figi"]: 1038 figiJSON = self.iList["Shares"][item] 1039 1040 if self.moreDebug: 1041 uLogger.debug("FIGI [{}] found in shares list".format(self._figi)) 1042 1043 break 1044 1045 if not figiJSON: 1046 for item in self.iList["Currencies"].keys(): 1047 if self._figi == self.iList["Currencies"][item]["figi"]: 1048 figiJSON = self.iList["Currencies"][item] 1049 1050 if self.moreDebug: 1051 uLogger.debug("FIGI [{}] found in currencies list".format(self._figi)) 1052 1053 break 1054 1055 if not figiJSON: 1056 for item in self.iList["Bonds"].keys(): 1057 if self._figi == self.iList["Bonds"][item]["figi"]: 1058 figiJSON = self.iList["Bonds"][item] 1059 1060 if self.moreDebug: 1061 uLogger.debug("FIGI [{}] found in bonds list".format(self._figi)) 1062 1063 break 1064 1065 if not figiJSON: 1066 for item in self.iList["Etfs"].keys(): 1067 if self._figi == self.iList["Etfs"][item]["figi"]: 1068 figiJSON = self.iList["Etfs"][item] 1069 1070 if self.moreDebug: 1071 uLogger.debug("FIGI [{}] found in etfs list".format(self._figi)) 1072 1073 break 1074 1075 if not figiJSON: 1076 for item in self.iList["Futures"].keys(): 1077 if self._figi == self.iList["Futures"][item]["figi"]: 1078 figiJSON = self.iList["Futures"][item] 1079 1080 if self.moreDebug: 1081 uLogger.debug("FIGI [{}] found in futures list".format(self._figi)) 1082 1083 break 1084 1085 if figiJSON: 1086 self._figi = figiJSON["figi"] 1087 self._ticker = figiJSON["ticker"] 1088 1089 if requestPrice: 1090 figiJSON["currentPrice"] = self.GetCurrentPrices(show=False) 1091 1092 if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None: 1093 figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"] 1094 1095 else: 1096 figiJSON["currentPrice"]["changes"] = 0 1097 1098 if show: 1099 self.ShowInstrumentInfo(iJSON=figiJSON, show=True) # print info as Markdown text 1100 1101 else: 1102 if show: 1103 uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self._figi)) 1104 1105 return figiJSON 1106 1107 def GetCurrentPrices(self, show: bool = True) -> dict: 1108 """ 1109 Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5: 1110 `{"buy": [{"price": 1243.8, "quantity": 193}, 1111 {"price": 1244.0, "quantity": 168}, 1112 {"price": 1244.8, "quantity": 5}, 1113 {"price": 1245.0, "quantity": 61}, 1114 {"price": 1245.4, "quantity": 60}], 1115 "sell": [{"price": 1243.6, "quantity": 8}, 1116 {"price": 1242.6, "quantity": 10}, 1117 {"price": 1242.4, "quantity": 18}, 1118 {"price": 1242.2, "quantity": 50}, 1119 {"price": 1242.0, "quantity": 113}], 1120 "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean: 1121 - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order 1122 - sell: list of dicts with Buyers prices, 1123 - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument), 1124 - quantity: volume value by current price in lots, 1125 - limitUp: current trade session limit price, maximum, 1126 - limitDown: current trade session limit price, minimum, 1127 - lastPrice: last deal price of the instrument, 1128 - closePrice: previous trade session close price of the instrument. 1129 1130 See also: `SearchByTicker()` and `SearchByFIGI()`. 1131 REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1132 Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1133 1134 :param show: if `True` then print DOM to log and console. 1135 :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`. 1136 If an error occurred then returns an empty record: 1137 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`. 1138 """ 1139 prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0} 1140 1141 if self.depth < 1: 1142 uLogger.error("Depth of Market (DOM) must be >=1!") 1143 raise Exception("Incorrect value") 1144 1145 if not (self._ticker or self._figi): 1146 uLogger.error("self._ticker or self._figi variables must be defined!") 1147 raise Exception("Ticker or FIGI required") 1148 1149 if self._ticker and not self._figi: 1150 instrumentByTicker = self.SearchByTicker(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1151 self._figi = instrumentByTicker["figi"] if instrumentByTicker else "" 1152 1153 if not self._ticker and self._figi: 1154 instrumentByFigi = self.SearchByFIGI(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1155 self._ticker = instrumentByFigi["ticker"] if instrumentByFigi else "" 1156 1157 if not self._figi: 1158 uLogger.error("FIGI is not defined!") 1159 raise Exception("Ticker or FIGI required") 1160 1161 else: 1162 uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self._ticker, self._figi)) 1163 1164 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1165 priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook" 1166 self.body = str({"figi": self._figi, "depth": self.depth}) 1167 pricesResponse = self.SendAPIRequest(priceURL, reqType="POST") # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1168 1169 if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()): 1170 # list of dicts with sellers orders: 1171 prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]] 1172 1173 # list of dicts with buyers orders: 1174 prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]] 1175 1176 # max price of instrument at this time: 1177 prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None 1178 1179 # min price of instrument at this time: 1180 prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None 1181 1182 # last price of deal with instrument: 1183 prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0 1184 1185 # last close price of instrument: 1186 prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0 1187 1188 else: 1189 uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi)) 1190 uLogger.debug("Server response: {}".format(pricesResponse)) 1191 1192 if show: 1193 if prices["buy"] or prices["sell"]: 1194 info = [ 1195 "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format( 1196 datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 1197 self._ticker, 1198 self._figi, 1199 self.depth, 1200 ), 1201 "-" * 60, "\n", 1202 " Orders of Buyers | Orders of Sellers\n", 1203 "-" * 60, "\n", 1204 " Sell prices (volumes) | Buy prices (volumes)\n", 1205 "-" * 60, "\n", 1206 ] 1207 1208 if not prices["buy"]: 1209 info.append(" | No orders!\n") 1210 sumBuy = 0 1211 1212 else: 1213 sumBuy = sum([x["quantity"] for x in prices["buy"]]) 1214 maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True) 1215 for item in maxMinSorted: 1216 info.append(" | {} ({})\n".format(item["price"], item["quantity"])) 1217 1218 if not prices["sell"]: 1219 info.append("No orders! |\n") 1220 sumSell = 0 1221 1222 else: 1223 sumSell = sum([x["quantity"] for x in prices["sell"]]) 1224 for item in prices["sell"]: 1225 info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"]))) 1226 1227 info.extend([ 1228 "-" * 60, "\n", 1229 "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)), 1230 "-" * 60, "\n", 1231 ]) 1232 1233 infoText = "".join(info) 1234 1235 uLogger.info("Current prices in order book:\n\n{}".format(infoText)) 1236 1237 else: 1238 uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi)) 1239 1240 return prices 1241 1242 def ShowInstrumentsInfo(self, show: bool = True, onlyFiles=False) -> str: 1243 """ 1244 This method get and show information about all available broker instruments for current user account. 1245 If `instrumentsFile` string is not empty then also save information to this file. 1246 1247 :param show: if `True` then print results to console, if `False` — print only to file. 1248 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 1249 :return: multi-lines string with all available broker instruments. 1250 """ 1251 if not self.iList: 1252 self.iList = self.Listing() 1253 1254 info = [ 1255 "# All available instruments from Tinkoff Broker server for current user token\n\n", 1256 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 1257 ] 1258 1259 # add instruments count by type: 1260 for iType in self.iList.keys(): 1261 info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType]))) 1262 1263 headerLine = "| Ticker | Full name | FIGI | Cur | Lot | Step |\n" 1264 splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n" 1265 1266 # generating info tables with all instruments by type: 1267 for iType in self.iList.keys(): 1268 info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine]) 1269 1270 for instrument in self.iList[iType].keys(): 1271 iName = self.iList[iType][instrument]["name"] # instrument's name 1272 if len(iName) > 57: 1273 iName = "{}...".format(iName[:54]) # right trim for a long string 1274 1275 info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format( 1276 self.iList[iType][instrument]["ticker"], 1277 iName, 1278 self.iList[iType][instrument]["figi"], 1279 self.iList[iType][instrument]["currency"], 1280 self.iList[iType][instrument]["lot"], 1281 "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0, 1282 )) 1283 1284 infoText = "".join(info) 1285 1286 if show and not onlyFiles: 1287 uLogger.info(infoText) 1288 1289 if self.instrumentsFile and (show or onlyFiles): 1290 with open(self.instrumentsFile, "w", encoding="UTF-8") as fH: 1291 fH.write(infoText) 1292 1293 uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile))) 1294 1295 if self.useHTMLReports: 1296 htmlFilePath = self.instrumentsFile.replace(".md", ".html") if self.instrumentsFile.endswith(".md") else self.instrumentsFile + ".html" 1297 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1298 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="List of instruments", commonCSS=COMMON_CSS, markdown=infoText)) 1299 1300 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1301 1302 return infoText 1303 1304 def SearchInstruments(self, pattern: str, show: bool = True, onlyFiles=False) -> dict: 1305 """ 1306 This method search and show information about instruments by part of its ticker, FIGI or name. 1307 If `searchResultsFile` string is not empty then also save information to this file. 1308 1309 :param pattern: string with part of ticker, FIGI or instrument's name. 1310 :param show: if `True` then print results to console, if `False` — return list of result only. 1311 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 1312 :return: list of dictionaries with all found instruments. 1313 """ 1314 if not self.iList: 1315 self.iList = self.Listing() 1316 1317 searchResults = {iType: {} for iType in self.iList} # same as iList but will contain only filtered instruments 1318 compiledPattern = re.compile(pattern, re.IGNORECASE) 1319 1320 for iType in self.iList: 1321 for instrument in self.iList[iType].values(): 1322 searchResult = compiledPattern.search(" ".join( 1323 [instrument["ticker"], instrument["figi"], instrument["name"]] 1324 )) 1325 1326 if searchResult: 1327 searchResults[iType][instrument["ticker"]] = instrument 1328 1329 resultsLen = sum([len(searchResults[iType]) for iType in searchResults]) 1330 info = [ 1331 "# Search results\n\n", 1332 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 1333 "* **Search pattern:** [{}]\n".format(pattern), 1334 "* **Found instruments:** [{}]\n\n".format(resultsLen), 1335 '**Note:** you can view info about found instruments with key "--info", e.g.: "tksbrokerapi -t TICKER --info" or "tksbrokerapi -f FIGI --info".\n' 1336 ] 1337 infoShort = info[:] 1338 1339 headerLine = "| Type | Ticker | Full name | FIGI |\n" 1340 splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n" 1341 skippedLine = "| ... | ... | ... | ... |\n" 1342 1343 if resultsLen == 0: 1344 info.append("\nNo results\n") 1345 infoShort.append("\nNo results\n") 1346 uLogger.warning("No results. Try changing your search pattern.") 1347 1348 else: 1349 for iType in searchResults: 1350 iTypeValuesCount = len(searchResults[iType].values()) 1351 if iTypeValuesCount > 0: 1352 info.extend(["\n## {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1353 infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1354 1355 for instrument in searchResults[iType].values(): 1356 info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format( 1357 instrument["type"], 1358 instrument["ticker"], 1359 "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"], # right trim for a long string 1360 instrument["figi"], 1361 )) 1362 1363 if iTypeValuesCount <= 5: 1364 infoShort.extend(info[-iTypeValuesCount:]) 1365 1366 else: 1367 infoShort.extend(info[-5:]) 1368 infoShort.append(skippedLine) 1369 1370 infoText = "".join(info) 1371 infoTextShort = "".join(infoShort) 1372 1373 if show and not onlyFiles: 1374 uLogger.info(infoTextShort) 1375 uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`") 1376 1377 if self.searchResultsFile and (show or onlyFiles): 1378 with open(self.searchResultsFile, "w", encoding="UTF-8") as fH: 1379 fH.write(infoText) 1380 1381 uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile))) 1382 1383 if self.useHTMLReports: 1384 htmlFilePath = self.searchResultsFile.replace(".md", ".html") if self.searchResultsFile.endswith(".md") else self.searchResultsFile + ".html" 1385 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1386 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Search results", commonCSS=COMMON_CSS, markdown=infoText)) 1387 1388 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1389 1390 return searchResults 1391 1392 def GetUniqueFIGIs(self, instruments: list[str]) -> list: 1393 """ 1394 Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs. 1395 1396 :param instruments: list of strings with tickers or FIGIs. 1397 :return: list with unique instrument FIGIs only. 1398 """ 1399 requestedInstruments = [] 1400 for iName in instruments: 1401 if iName not in self.aliases.keys(): 1402 if iName not in requestedInstruments: 1403 requestedInstruments.append(iName) 1404 1405 else: 1406 if iName not in requestedInstruments: 1407 if self.aliases[iName] not in requestedInstruments: 1408 requestedInstruments.append(self.aliases[iName]) 1409 1410 uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments)) 1411 1412 onlyUniqueFIGIs = [] 1413 for iName in requestedInstruments: 1414 if iName in TKS_TICKERS_OR_FIGI_EXCLUDED: 1415 continue 1416 1417 self._ticker = iName 1418 iData = self.SearchByTicker(requestPrice=False) # trying to find instrument by ticker 1419 1420 if not iData: 1421 self._ticker = "" 1422 self._figi = iName 1423 1424 iData = self.SearchByFIGI(requestPrice=False) # trying to find instrument by FIGI 1425 1426 if not iData: 1427 self._figi = "" 1428 uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName)) 1429 1430 if iData and iData["figi"] not in onlyUniqueFIGIs: 1431 onlyUniqueFIGIs.append(iData["figi"]) 1432 1433 uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs)) 1434 1435 return onlyUniqueFIGIs 1436 1437 def GetListOfPrices(self, instruments: list[str], show: bool = False, onlyFiles=False) -> list[dict]: 1438 """ 1439 This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation! 1440 1441 See limits: https://tinkoff.github.io/investAPI/limits/ 1442 1443 If `pricesFile` string is not empty then also save information to this file. 1444 1445 :param instruments: list of strings with tickers or FIGIs. 1446 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1447 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 1448 :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1449 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods. 1450 """ 1451 if instruments is None or not instruments: 1452 uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!") 1453 raise Exception("Ticker or FIGI required") 1454 1455 onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments) 1456 1457 uLogger.debug("Requesting current prices from Tinkoff Broker server...") 1458 1459 iList = [] # trying to get info and current prices about all unique instruments: 1460 for self._figi in onlyUniqueFIGIs: 1461 iData = self.SearchByFIGI(requestPrice=True, show=False) 1462 iList.append(iData) 1463 1464 self.ShowListOfPrices(iList, show, onlyFiles) 1465 1466 return iList 1467 1468 def ShowListOfPrices(self, iList: list, show: bool = True, onlyFiles=False) -> str: 1469 """ 1470 Show table contains current prices of given instruments. 1471 1472 :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1473 One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods. 1474 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1475 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 1476 :return: multilines text in Markdown format as a table contains current prices. 1477 """ 1478 infoText = "" 1479 1480 if show or self.pricesFile or onlyFiles: 1481 info = [ 1482 "# Current prices\n\n* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1483 "| Ticker | FIGI | Type | Prev. close | Last price | Chg. % | Day limits min/max | Actual sell / buy | Curr. |\n", 1484 "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n", 1485 ] 1486 1487 for item in iList: 1488 info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format( 1489 item["ticker"], 1490 item["figi"], 1491 item["type"], 1492 "{:.2f}".format(float(item["currentPrice"]["closePrice"])), 1493 "{:.2f}".format(float(item["currentPrice"]["lastPrice"])), 1494 "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])), 1495 "{} / {}".format( 1496 item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A", 1497 item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A", 1498 ), 1499 "{} / {}".format( 1500 item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A", 1501 item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A", 1502 ), 1503 item["currency"], 1504 )) 1505 1506 infoText = "".join(info) 1507 1508 if show and not onlyFiles: 1509 uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText)) 1510 1511 if self.pricesFile and (show or onlyFiles): 1512 with open(self.pricesFile, "w", encoding="UTF-8") as fH: 1513 fH.write(infoText) 1514 1515 uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile))) 1516 1517 if self.useHTMLReports: 1518 htmlFilePath = self.pricesFile.replace(".md", ".html") if self.pricesFile.endswith(".md") else self.pricesFile + ".html" 1519 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1520 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Current prices", commonCSS=COMMON_CSS, markdown=infoText)) 1521 1522 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1523 1524 return infoText 1525 1526 def RequestTradingStatus(self) -> dict: 1527 """ 1528 Requesting trading status for the instrument defined by `figi` variable. 1529 1530 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus 1531 1532 Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest 1533 1534 :return: dictionary with trading status attributes. Response example: 1535 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", 1536 "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}` 1537 """ 1538 if self._figi is None or not self._figi: 1539 uLogger.error("Variable `figi` must be defined for using this method!") 1540 raise Exception("FIGI required") 1541 1542 uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self._figi)) 1543 1544 self.body = str({"figi": self._figi, "instrumentId": self._figi}) 1545 tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus" 1546 tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST") 1547 1548 if self.moreDebug: 1549 uLogger.debug("Records about current trading status successfully received") 1550 1551 return tradingStatus 1552 1553 def RequestPortfolio(self) -> dict: 1554 """ 1555 Requesting actual user's portfolio for current `accountId`. 1556 1557 REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio 1558 1559 Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest 1560 1561 :return: dictionary with user's portfolio. 1562 """ 1563 if self.accountId is None or not self.accountId: 1564 uLogger.error("Variable `accountId` must be defined for using this method!") 1565 raise Exception("Account ID required") 1566 1567 uLogger.debug("Requesting current actual user's portfolio. Wait, please...") 1568 1569 self.body = str({"accountId": self.accountId}) 1570 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio" 1571 rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST") 1572 1573 if self.moreDebug: 1574 uLogger.debug("Records about user's portfolio successfully received") 1575 1576 return rawPortfolio 1577 1578 def RequestPositions(self) -> dict: 1579 """ 1580 Requesting open positions by currencies and instruments for current `accountId`. 1581 1582 REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions 1583 1584 Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest 1585 1586 :return: dictionary with open positions by instruments. 1587 """ 1588 if self.accountId is None or not self.accountId: 1589 uLogger.error("Variable `accountId` must be defined for using this method!") 1590 raise Exception("Account ID required") 1591 1592 uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...") 1593 1594 self.body = str({"accountId": self.accountId}) 1595 positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions" 1596 rawPositions = self.SendAPIRequest(positionsURL, reqType="POST") 1597 1598 if self.moreDebug: 1599 uLogger.debug("Records about current open positions successfully received") 1600 1601 return rawPositions 1602 1603 def RequestPendingOrders(self) -> list: 1604 """ 1605 Requesting current actual pending limit orders for current `accountId`. 1606 1607 REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders 1608 1609 Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest 1610 1611 :return: list of dictionaries with pending limit orders. 1612 """ 1613 if self.accountId is None or not self.accountId: 1614 uLogger.error("Variable `accountId` must be defined for using this method!") 1615 raise Exception("Account ID required") 1616 1617 uLogger.debug("Requesting current actual pending limit orders. Wait, please...") 1618 1619 self.body = str({"accountId": self.accountId}) 1620 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders" 1621 rawResponse = self.SendAPIRequest(ordersURL, reqType="POST") 1622 1623 if "orders" in rawResponse.keys(): 1624 rawOrders = rawResponse["orders"] 1625 uLogger.debug("[{}] records about pending limit orders received".format(len(rawOrders))) 1626 1627 else: 1628 rawOrders = [] 1629 uLogger.debug("No pending limit orders returned! rawResponse = {}".format(rawResponse)) 1630 1631 return rawOrders 1632 1633 def RequestStopOrders(self) -> list: 1634 """ 1635 Requesting current actual stop orders for current `accountId`. 1636 1637 REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders 1638 1639 Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest 1640 1641 :return: list of dictionaries with stop orders. 1642 """ 1643 if self.accountId is None or not self.accountId: 1644 uLogger.error("Variable `accountId` must be defined for using this method!") 1645 raise Exception("Account ID required") 1646 1647 uLogger.debug("Requesting current actual stop orders. Wait, please...") 1648 1649 self.body = str({"accountId": self.accountId}) 1650 stopOrdersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders" 1651 rawResponse = self.SendAPIRequest(stopOrdersURL, reqType="POST") 1652 1653 if "stopOrders" in rawResponse.keys(): 1654 rawStopOrders = rawResponse["stopOrders"] 1655 uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders))) 1656 1657 else: 1658 rawStopOrders = [] 1659 uLogger.debug("No stop orders returned! rawResponse = {}".format(rawResponse)) 1660 1661 return rawStopOrders 1662 1663 def Overview(self, show: bool = False, details: str = "full", onlyFiles=False) -> dict: 1664 """ 1665 Get portfolio: all open positions, orders and some statistics for current `accountId`. 1666 If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile` 1667 and `overviewBondsCalendarFile` are defined then also save information to file. 1668 1669 WARNING! It is not recommended to run this method too many times in a loop! The server receives 1670 many requests about the state of the portfolio, and then, based on the received data, a large number 1671 of calculation and statistics are collected. 1672 1673 :param show: if `False` then only dictionary returns, if `True` then show more debug information. 1674 :param details: how detailed should the information be? 1675 - `full` — shows full available information about portfolio status (by default), 1676 - `positions` — shows only open positions, 1677 - `orders` — shows only sections of open limits and stop orders. 1678 - `digest` — show a short digest of the portfolio status, 1679 - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories, 1680 - `calendar` — shows only the bonds calendar section (if these present in portfolio). 1681 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 1682 :return: dictionary with client's raw portfolio and some statistics. 1683 """ 1684 if self.accountId is None or not self.accountId: 1685 uLogger.error("Variable `accountId` must be defined for using this method!") 1686 raise Exception("Account ID required") 1687 1688 view = { 1689 "raw": { # --- raw portfolio responses from broker with user portfolio data: 1690 "headers": {}, # list of dictionaries, response headers without "positions" section 1691 "Currencies": [], # list of dictionaries, open trades with currencies from "positions" section 1692 "Shares": [], # list of dictionaries, open trades with shares from "positions" section 1693 "Bonds": [], # list of dictionaries, open trades with bonds from "positions" section 1694 "Etfs": [], # list of dictionaries, open trades with etfs from "positions" section 1695 "Futures": [], # list of dictionaries, open trades with futures from "positions" section 1696 "positions": {}, # raw response from broker: dictionary with current available or blocked currencies and instruments for client 1697 "orders": [], # raw response from broker: list of dictionaries with all pending (market) orders 1698 "stopOrders": [], # raw response from broker: list of dictionaries with all stop orders 1699 "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}}, # dict with prices of all currencies in RUB 1700 }, 1701 "stat": { # --- some statistics calculated using "raw" sections: 1702 "portfolioCostRUB": 0., # portfolio cost in RUB (Russian Rouble) 1703 "availableRUB": 0., # available rubles (without other currencies) 1704 "blockedRUB": 0., # blocked sum in Russian Rouble 1705 "totalChangesRUB": 0., # changes for all open trades in RUB 1706 "totalChangesPercentRUB": 0., # changes for all open trades in percents 1707 "allCurrenciesCostRUB": 0., # costs of all currencies (include rubles) in RUB 1708 "sharesCostRUB": 0., # costs of all shares in RUB 1709 "bondsCostRUB": 0., # costs of all bonds in RUB 1710 "etfsCostRUB": 0., # costs of all etfs in RUB 1711 "futuresCostRUB": 0., # costs of all futures in RUB 1712 "Currencies": [], # list of dictionaries of all currencies statistics 1713 "Shares": [], # list of dictionaries of all shares statistics 1714 "Bonds": [], # list of dictionaries of all bonds statistics 1715 "Etfs": [], # list of dictionaries of all etfs statistics 1716 "Futures": [], # list of dictionaries of all futures statistics 1717 "orders": [], # list of dictionaries of all pending (market) orders and it's parameters 1718 "stopOrders": [], # list of dictionaries of all stop orders and it's parameters 1719 "blockedCurrencies": {}, # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21} 1720 "blockedInstruments": {}, # dict with blocked by FIGI, e.g. {} 1721 "funds": {}, # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1722 }, 1723 "analytics": { # --- some analytics of portfolio: 1724 "distrByAssets": {}, # portfolio distribution by assets 1725 "distrByCompanies": {}, # portfolio distribution by companies 1726 "distrBySectors": {}, # portfolio distribution by sectors 1727 "distrByCurrencies": {}, # portfolio distribution by currencies 1728 "distrByCountries": {}, # portfolio distribution by countries 1729 "bondsCalendar": None, # bonds payment calendar as Pandas DataFrame (if these present in portfolio) 1730 } 1731 } 1732 1733 details = details.lower() 1734 availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"] 1735 if details not in availableDetails: 1736 details = "full" 1737 uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails)) 1738 1739 uLogger.debug("Requesting portfolio of a client. Wait, please...") 1740 1741 portfolioResponse = self.RequestPortfolio() # current user's portfolio (dict) 1742 view["raw"]["positions"] = self.RequestPositions() # current open positions by instruments (dict) 1743 view["raw"]["orders"] = self.RequestPendingOrders() # current actual pending limit orders (list) 1744 view["raw"]["stopOrders"] = self.RequestStopOrders() # current actual stop orders (list) 1745 1746 # save response headers without "positions" section: 1747 for key in portfolioResponse.keys(): 1748 if key != "positions": 1749 view["raw"]["headers"][key] = portfolioResponse[key] 1750 1751 else: 1752 continue 1753 1754 # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation 1755 # Type of instrument must be only one of supported types in TKS_INSTRUMENTS 1756 for item in portfolioResponse["positions"]: 1757 if item["instrumentType"] == "currency": 1758 self._figi = item["figi"] 1759 if not self._figi and item["ticker"]: 1760 self._ticker = item["ticker"] 1761 self._figi = self.SearchByTicker()["figi"] # Get FIGI to avoid warnings 1762 1763 curr = self.SearchByFIGI(requestPrice=False) 1764 1765 # current price of currency in RUB: 1766 view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = { 1767 "name": curr["name"], 1768 "currentPrice": NanoToFloat( 1769 item["currentPrice"]["units"], 1770 item["currentPrice"]["nano"] 1771 ), 1772 } 1773 1774 view["raw"]["Currencies"].append(item) 1775 1776 elif item["instrumentType"] == "share": 1777 view["raw"]["Shares"].append(item) 1778 1779 elif item["instrumentType"] == "bond": 1780 view["raw"]["Bonds"].append(item) 1781 1782 elif item["instrumentType"] == "etf": 1783 view["raw"]["Etfs"].append(item) 1784 1785 elif item["instrumentType"] == "futures": 1786 view["raw"]["Futures"].append(item) 1787 1788 else: 1789 continue 1790 1791 # how many volume of currencies (by ISO currency name) are blocked: 1792 for item in view["raw"]["positions"]["blocked"]: 1793 blocked = NanoToFloat(item["units"], item["nano"]) 1794 if blocked > 0: 1795 view["stat"]["blockedCurrencies"][item["currency"]] = blocked 1796 1797 # how many volume of instruments (by FIGI) are blocked: 1798 for item in view["raw"]["positions"]["securities"]: 1799 blocked = int(item["blocked"]) 1800 if blocked > 0: 1801 view["stat"]["blockedInstruments"][item["figi"]] = blocked 1802 1803 allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]} 1804 1805 if "rub" in allBlocked.keys(): 1806 view["stat"]["blockedRUB"] = allBlocked["rub"] # blocked rubles 1807 1808 # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies: 1809 view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"]) 1810 view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"]) 1811 view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"]) 1812 view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"]) 1813 view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"]) 1814 view["stat"]["portfolioCostRUB"] = sum([ 1815 view["stat"]["allCurrenciesCostRUB"], 1816 view["stat"]["sharesCostRUB"], 1817 view["stat"]["bondsCostRUB"], 1818 view["stat"]["etfsCostRUB"], 1819 view["stat"]["futuresCostRUB"], 1820 ]) 1821 1822 # --- calculating some portfolio statistics: 1823 byComp = {} # distribution by companies 1824 bySect = {} # distribution by sectors 1825 byCurr = {} # distribution by currencies (include RUB) 1826 unknownCountryName = "All other countries" # default name for instruments without "countryOfRisk" and "countryOfRiskName" 1827 byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}} # distribution by countries (currencies are included in their countries) 1828 1829 for item in portfolioResponse["positions"]: 1830 self._figi = item["figi"] 1831 if not self._figi and item["ticker"]: 1832 self._ticker = item["ticker"] 1833 self._figi = self.SearchByTicker()["figi"] # Get FIGI to avoid warnings 1834 1835 instrument = self.SearchByFIGI(requestPrice=False) # full raw info about instrument by FIGI 1836 1837 if instrument: 1838 if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys(): 1839 blocked = allBlocked[instrument["nominal"]["currency"]] # blocked volume of currency 1840 1841 elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys(): 1842 blocked = allBlocked[item["figi"]] # blocked volume of other instruments 1843 1844 else: 1845 blocked = 0 1846 1847 volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"]) # available volume of instrument 1848 lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"]) # available volume in lots of instrument 1849 direction = "Long" if lots >= 0 else "Short" # direction of an instrument's position: short or long 1850 curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"]) # current instrument's price 1851 average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"]) # current average position price 1852 profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"]) # expected profit at current moment 1853 currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"] # currency name rub, usd, eur etc. 1854 cost = curPrice if "currentNkd" not in item.keys() else (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume # current cost of all volume of instrument in basic asset 1855 baseCurrencyName = item["currentPrice"]["currency"] # name of base currency (rub) 1856 countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName 1857 costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"] # cost in rubles 1858 percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0. # instrument's part in percent of full portfolio cost 1859 1860 statData = { 1861 "figi": item["figi"], # FIGI from REST API "GetPortfolio" method 1862 "ticker": instrument["ticker"], # ticker by FIGI 1863 "currency": currency, # currency name rub, usd, eur etc. for instrument price 1864 "volume": volume, # available volume of instrument 1865 "lots": lots, # volume in lots of instrument 1866 "direction": direction, # direction of an instrument's position: short or long 1867 "blocked": blocked, # blocked volume of currency or instrument 1868 "currentPrice": curPrice, # current instrument's price in basic asset 1869 "average": average, # current average position price 1870 "cost": cost, # current cost of all volume of instrument in basic asset 1871 "baseCurrencyName": baseCurrencyName, # name of base currency (rub) 1872 "costRUB": costRUB, # cost of instrument in ruble 1873 "percentCostRUB": percentCostRUB, # instrument's part in percent of full portfolio cost in RUB 1874 "profit": profit, # expected profit at current moment 1875 "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0, # expected percents of profit at current moment for this instrument 1876 "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other", 1877 "name": instrument["name"] if "name" in instrument.keys() else "", # human-readable names of instruments 1878 "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "", # ISO name for currencies only 1879 "country": countryName, # e.g. "[RU] Российская Федерация" or unknownCountryName 1880 "step": instrument["step"], # minimum price increment 1881 } 1882 1883 # adding distribution by unique countries: 1884 if statData["country"] not in byCountry.keys(): 1885 byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB} 1886 1887 else: 1888 byCountry[statData["country"]]["cost"] += costRUB 1889 byCountry[statData["country"]]["percent"] += percentCostRUB 1890 1891 if item["instrumentType"] != "currency": 1892 # adding distribution by unique companies: 1893 if statData["name"]: 1894 if statData["name"] not in byComp.keys(): 1895 byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB} 1896 1897 else: 1898 byComp[statData["name"]]["cost"] += costRUB 1899 byComp[statData["name"]]["percent"] += percentCostRUB 1900 1901 # adding distribution by unique sectors: 1902 if statData["sector"] not in bySect.keys(): 1903 bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB} 1904 1905 else: 1906 bySect[statData["sector"]]["cost"] += costRUB 1907 bySect[statData["sector"]]["percent"] += percentCostRUB 1908 1909 # adding distribution by unique currencies: 1910 if currency not in byCurr.keys(): 1911 byCurr[currency] = { 1912 "name": view["raw"]["currenciesCurrentPrices"][currency]["name"], 1913 "cost": costRUB, 1914 "percent": percentCostRUB 1915 } 1916 1917 else: 1918 byCurr[currency]["cost"] += costRUB 1919 byCurr[currency]["percent"] += percentCostRUB 1920 1921 # saving statistics for every instrument: 1922 if item["instrumentType"] == "currency": 1923 view["stat"]["Currencies"].append(statData) 1924 1925 # update dict with free funds for trading (total - blocked) by currencies 1926 # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1927 view["stat"]["funds"][currency] = { 1928 "total": volume, 1929 "totalCostRUB": costRUB, # total volume cost in rubles 1930 "free": volume - blocked, 1931 "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0, # free volume cost in rubles 1932 } 1933 1934 elif item["instrumentType"] == "share": 1935 view["stat"]["Shares"].append(statData) 1936 1937 elif item["instrumentType"] == "bond": 1938 view["stat"]["Bonds"].append(statData) 1939 1940 elif item["instrumentType"] == "etf": 1941 view["stat"]["Etfs"].append(statData) 1942 1943 elif item["instrumentType"] == "Futures": 1944 view["stat"]["Futures"].append(statData) 1945 1946 else: 1947 continue 1948 1949 # total changes in Russian Ruble: 1950 view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]]) # available RUB without other currencies 1951 view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0. 1952 startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100) 1953 view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost 1954 view["stat"]["funds"]["rub"] = { 1955 "total": view["stat"]["availableRUB"], 1956 "totalCostRUB": view["stat"]["availableRUB"], 1957 "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1958 "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1959 } 1960 1961 # --- pending limit orders sector data: 1962 uniquePendingOrdersFIGIs = [] # unique FIGIs of pending limit orders to avoid many times price requests 1963 uniquePendingOrders = {} # unique instruments with FIGIs as dictionary keys 1964 1965 for item in view["raw"]["orders"]: 1966 self._figi = item["figi"] 1967 1968 if item["figi"] not in uniquePendingOrdersFIGIs: 1969 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1970 1971 uniquePendingOrdersFIGIs.append(item["figi"]) 1972 uniquePendingOrders[item["figi"]] = instrument 1973 1974 else: 1975 instrument = uniquePendingOrders[item["figi"]] 1976 1977 if instrument: 1978 action = TKS_ORDER_DIRECTIONS[item["direction"]] 1979 orderType = TKS_ORDER_TYPES[item["orderType"]] 1980 orderState = TKS_ORDER_STATES[item["executionReportStatus"]] 1981 orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1982 1983 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1984 if item["direction"] == "ORDER_DIRECTION_BUY": 1985 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1986 1987 else: 1988 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1989 1990 # requested price for order execution: 1991 target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"]) 1992 1993 # necessary changes in percent to reach target from current price: 1994 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1995 1996 view["stat"]["orders"].append({ 1997 "orderID": item["orderId"], # orderId number parameter of current order 1998 "figi": item["figi"], # FIGI identification 1999 "ticker": instrument["ticker"], # ticker name by FIGI 2000 "lotsRequested": item["lotsRequested"], # requested lots value 2001 "lotsExecuted": item["lotsExecuted"], # how many lots are executed 2002 "currentPrice": lastPrice, # current instrument's price for defined action 2003 "targetPrice": target, # requested price for order execution in base currency 2004 "baseCurrencyName": item["initialSecurityPrice"]["currency"], # name of base currency 2005 "percentChanges": changes, # changes in percent to target from current price 2006 "currency": item["currency"], # instrument's currency name 2007 "action": action, # sell / buy / Unknown from TKS_ORDER_DIRECTIONS 2008 "type": orderType, # type of order from TKS_ORDER_TYPES 2009 "status": orderState, # order status from TKS_ORDER_STATES 2010 "date": orderDate, # string with order date and time from UTC format (without nano seconds part) 2011 }) 2012 2013 # --- stop orders sector data: 2014 uniqueStopOrdersFIGIs = [] # unique FIGIs of stop orders to avoid many times price requests 2015 uniqueStopOrders = {} # unique instruments with FIGIs as dictionary keys 2016 2017 for item in view["raw"]["stopOrders"]: 2018 self._figi = item["figi"] 2019 2020 if item["figi"] not in uniqueStopOrdersFIGIs: 2021 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 2022 2023 uniqueStopOrdersFIGIs.append(item["figi"]) 2024 uniqueStopOrders[item["figi"]] = instrument 2025 2026 else: 2027 instrument = uniqueStopOrders[item["figi"]] 2028 2029 if instrument: 2030 action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]] 2031 orderType = TKS_STOP_ORDER_TYPES[item["orderType"]] 2032 createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 2033 2034 # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order 2035 if "expirationTime" in item.keys(): 2036 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"] 2037 expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0] 2038 2039 else: 2040 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"] 2041 expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"] 2042 2043 # current instrument's price (last sellers order if buy, and last buyers order if sell): 2044 if item["direction"] == "STOP_ORDER_DIRECTION_BUY": 2045 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 2046 2047 else: 2048 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 2049 2050 # requested price when stop-order executed: 2051 target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"]) 2052 2053 # price for limit-order, set up when stop-order executed: 2054 limit = NanoToFloat(item["price"]["units"], item["price"]["nano"]) 2055 2056 # necessary changes in percent to reach target from current price: 2057 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 2058 2059 view["stat"]["stopOrders"].append({ 2060 "orderID": item["stopOrderId"], # stopOrderId number parameter of current stop-order 2061 "figi": item["figi"], # FIGI identification 2062 "ticker": instrument["ticker"], # ticker name by FIGI 2063 "lotsRequested": item["lotsRequested"], # requested lots value 2064 "currentPrice": lastPrice, # current instrument's price for defined action 2065 "targetPrice": target, # requested price for stop-order execution in base currency 2066 "limitPrice": limit, # price for limit-order, set up when stop-order executed, 0 if market order 2067 "baseCurrencyName": item["stopPrice"]["currency"], # name of base currency 2068 "percentChanges": changes, # changes in percent to target from current price 2069 "currency": item["currency"], # instrument's currency name 2070 "action": action, # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS 2071 "type": orderType, # type of order from TKS_STOP_ORDER_TYPES 2072 "expType": expType, # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES 2073 "createDate": createDate, # string with created order date and time from UTC format (without nano seconds part) 2074 "expDate": expDate, # string with expiration order date and time from UTC format (without nano seconds part) 2075 }) 2076 2077 # --- calculating data for analytics section: 2078 # portfolio distribution by assets: 2079 view["analytics"]["distrByAssets"] = { 2080 "Ruble": { 2081 "uniques": 1, 2082 "cost": view["stat"]["availableRUB"], 2083 "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2084 }, 2085 "Currencies": { 2086 "uniques": len(view["stat"]["Currencies"]), # all foreign currencies without RUB 2087 "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"], 2088 "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2089 }, 2090 "Shares": { 2091 "uniques": len(view["stat"]["Shares"]), 2092 "cost": view["stat"]["sharesCostRUB"], 2093 "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2094 }, 2095 "Bonds": { 2096 "uniques": len(view["stat"]["Bonds"]), 2097 "cost": view["stat"]["bondsCostRUB"], 2098 "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2099 }, 2100 "Etfs": { 2101 "uniques": len(view["stat"]["Etfs"]), 2102 "cost": view["stat"]["etfsCostRUB"], 2103 "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2104 }, 2105 "Futures": { 2106 "uniques": len(view["stat"]["Futures"]), 2107 "cost": view["stat"]["futuresCostRUB"], 2108 "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2109 }, 2110 } 2111 2112 # portfolio distribution by companies: 2113 view["analytics"]["distrByCompanies"]["All money cash"] = { 2114 "ticker": "", 2115 "cost": view["stat"]["allCurrenciesCostRUB"], 2116 "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2117 } 2118 view["analytics"]["distrByCompanies"].update(byComp) 2119 2120 # portfolio distribution by sectors: 2121 view["analytics"]["distrBySectors"]["All money cash"] = { 2122 "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"], 2123 "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"], 2124 } 2125 view["analytics"]["distrBySectors"].update(bySect) 2126 2127 # portfolio distribution by currencies: 2128 if "rub" not in view["analytics"]["distrByCurrencies"].keys(): 2129 view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0} 2130 2131 if self.moreDebug: 2132 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!") 2133 2134 view["analytics"]["distrByCurrencies"].update(byCurr) 2135 view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2136 view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2137 2138 # portfolio distribution by countries: 2139 if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys(): 2140 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0} 2141 2142 if self.moreDebug: 2143 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!") 2144 2145 view["analytics"]["distrByCountries"].update(byCountry) 2146 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2147 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2148 2149 # --- Prepare text statistics overview in human-readable: 2150 if show or onlyFiles: 2151 actualOnDate = datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) 2152 2153 # Whatever the value `details`, header not changes: 2154 info = [ 2155 "# Client's portfolio\n\n", 2156 "* **Actual on date:** [{} UTC]\n".format(actualOnDate), 2157 "* **Account ID:** [{}]\n".format(self.accountId), 2158 ] 2159 2160 if details in ["full", "positions", "digest"]: 2161 info.extend([ 2162 "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2163 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format( 2164 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2165 view["stat"]["totalChangesRUB"], 2166 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2167 view["stat"]["totalChangesPercentRUB"], 2168 ), 2169 ]) 2170 2171 if details in ["full", "positions"]: 2172 info.extend([ 2173 "## Open positions\n\n", 2174 "| Ticker [FIGI] | Volume (blocked) | Lots | Curr. price | Avg. price | Current volume cost | Profit (%) |\n", 2175 "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n", 2176 "| **Ruble:** | {:>31} | | | | | |\n".format( 2177 "{:.2f} ({:.2f}) rub".format( 2178 view["stat"]["availableRUB"], 2179 view["stat"]["blockedRUB"], 2180 ) 2181 ) 2182 ]) 2183 2184 def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list: 2185 return [ 2186 "| | | | | | | |\n", 2187 "| {:<27} | | | | | {:>19} | |\n".format( 2188 noTradeStr if noTradeStr else typeStr, 2189 "" if noTradeStr else "{:.2f} RUB".format(CostRUB), 2190 ), 2191 ] 2192 2193 def _InfoStr(data: dict, isCurr: bool = False) -> str: 2194 return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format( 2195 "{} [{}]".format(data["ticker"], data["figi"]), 2196 "{:.2f} ({:.2f}) {}".format( 2197 data["volume"], 2198 data["blocked"], 2199 data["currency"], 2200 ) if isCurr else "{:.0f} ({:.0f})".format( 2201 data["volume"], 2202 data["blocked"], 2203 ), 2204 "—" if isCurr else "{:.4f}".format(data["lots"]).rstrip("0").rstrip("."), 2205 "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a", 2206 "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a", 2207 "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]), 2208 "{}{:.2f} {} ({}{:.2f}%)".format( 2209 "+" if data["profit"] > 0 else "", 2210 data["profit"], data["baseCurrencyName"], 2211 "+" if data["percentProfit"] > 0 else "", 2212 data["percentProfit"], 2213 ), 2214 ) 2215 2216 # --- Show currencies section: 2217 if view["stat"]["Currencies"]: 2218 info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**")) 2219 for item in view["stat"]["Currencies"]: 2220 info.append(_InfoStr(item, isCurr=True)) 2221 2222 else: 2223 info.extend(_SplitStr(noTradeStr="**Currencies:** no trades")) 2224 2225 # --- Show shares section: 2226 if view["stat"]["Shares"]: 2227 info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**")) 2228 2229 for item in view["stat"]["Shares"]: 2230 info.append(_InfoStr(item)) 2231 2232 else: 2233 info.extend(_SplitStr(noTradeStr="**Shares:** no trades")) 2234 2235 # --- Show bonds section: 2236 if view["stat"]["Bonds"]: 2237 info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**")) 2238 2239 for item in view["stat"]["Bonds"]: 2240 info.append(_InfoStr(item)) 2241 2242 else: 2243 info.extend(_SplitStr(noTradeStr="**Bonds:** no trades")) 2244 2245 # --- Show etfs section: 2246 if view["stat"]["Etfs"]: 2247 info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**")) 2248 2249 for item in view["stat"]["Etfs"]: 2250 info.append(_InfoStr(item)) 2251 2252 else: 2253 info.extend(_SplitStr(noTradeStr="**Etfs:** no trades")) 2254 2255 # --- Show futures section: 2256 if view["stat"]["Futures"]: 2257 info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**")) 2258 2259 for item in view["stat"]["Futures"]: 2260 info.append(_InfoStr(item)) 2261 2262 else: 2263 info.extend(_SplitStr(noTradeStr="**Futures:** no trades")) 2264 2265 if details in ["full", "orders"]: 2266 # --- Show pending limit orders section: 2267 if view["stat"]["orders"]: 2268 info.extend([ 2269 "\n## Opened pending limit-orders: [{}]\n".format(len(view["stat"]["orders"])), 2270 "\n| Ticker [FIGI] | Order ID | Lots (exec.) | Current price (% delta) | Target price | Action | Type | Create date (UTC) |\n", 2271 "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n", 2272 ]) 2273 2274 for item in view["stat"]["orders"]: 2275 info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format( 2276 "{} [{}]".format(item["ticker"], item["figi"]), 2277 item["orderID"], 2278 "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]), 2279 "{} {} ({}{:.2f}%)".format( 2280 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2281 item["baseCurrencyName"], 2282 "+" if item["percentChanges"] > 0 else "", 2283 float(item["percentChanges"]), 2284 ), 2285 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2286 item["action"], 2287 item["type"], 2288 item["date"], 2289 )) 2290 2291 else: 2292 info.append("\n## Total pending limit-orders: [0]\n") 2293 2294 # --- Show stop orders section: 2295 if view["stat"]["stopOrders"]: 2296 info.extend([ 2297 "\n## Opened stop-orders: [{}]\n".format(len(view["stat"]["stopOrders"])), 2298 "\n| Ticker [FIGI] | Stop order ID | Lots | Current price (% delta) | Target price | Limit price | Action | Type | Expire type | Create date (UTC) | Expiration (UTC) |\n", 2299 "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n", 2300 ]) 2301 2302 for item in view["stat"]["stopOrders"]: 2303 info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format( 2304 "{} [{}]".format(item["ticker"], item["figi"]), 2305 item["orderID"], 2306 item["lotsRequested"], 2307 "{} {} ({}{:.2f}%)".format( 2308 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2309 item["baseCurrencyName"], 2310 "+" if item["percentChanges"] > 0 else "", 2311 float(item["percentChanges"]), 2312 ), 2313 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2314 "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"], 2315 item["action"], 2316 item["type"], 2317 item["expType"], 2318 item["createDate"], 2319 item["expDate"], 2320 )) 2321 2322 else: 2323 info.append("\n## Total stop-orders: [0]\n") 2324 2325 if details in ["full", "analytics"]: 2326 # -- Show analytics section: 2327 if view["stat"]["portfolioCostRUB"] > 0: 2328 info.extend([ 2329 "\n# Analytics\n\n" 2330 "* **Actual on date:** [{} UTC]\n".format(actualOnDate), 2331 "* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2332 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format( 2333 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2334 view["stat"]["totalChangesRUB"], 2335 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2336 view["stat"]["totalChangesPercentRUB"], 2337 ), 2338 "\n## Portfolio distribution by assets\n" 2339 "\n| Type | Uniques | Percent | Current cost |\n", 2340 "|------------------------------------|---------|---------|--------------------|\n", 2341 ]) 2342 2343 for key in view["analytics"]["distrByAssets"].keys(): 2344 if view["analytics"]["distrByAssets"][key]["cost"] > 0: 2345 info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format( 2346 key, 2347 view["analytics"]["distrByAssets"][key]["uniques"], 2348 "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]), 2349 "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]), 2350 )) 2351 2352 aSepLine = "|----------------------------------------------|---------|--------------------|\n" 2353 2354 info.extend([ 2355 "\n## Portfolio distribution by companies\n" 2356 "\n| Company | Percent | Current cost |\n", 2357 aSepLine, 2358 ]) 2359 2360 for company in view["analytics"]["distrByCompanies"].keys(): 2361 if view["analytics"]["distrByCompanies"][company]["cost"] > 0: 2362 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2363 "{}{}".format( 2364 "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "", 2365 company, 2366 ), 2367 "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]), 2368 "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]), 2369 )) 2370 2371 info.extend([ 2372 "\n## Portfolio distribution by sectors\n" 2373 "\n| Sector | Percent | Current cost |\n", 2374 aSepLine, 2375 ]) 2376 2377 for sector in view["analytics"]["distrBySectors"].keys(): 2378 if view["analytics"]["distrBySectors"][sector]["cost"] > 0: 2379 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2380 sector, 2381 "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]), 2382 "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]), 2383 )) 2384 2385 info.extend([ 2386 "\n## Portfolio distribution by currencies\n" 2387 "\n| Instruments currencies | Percent | Current cost |\n", 2388 aSepLine, 2389 ]) 2390 2391 for curr in view["analytics"]["distrByCurrencies"].keys(): 2392 if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0: 2393 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2394 "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]), 2395 "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]), 2396 "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]), 2397 )) 2398 2399 info.extend([ 2400 "\n## Portfolio distribution by countries\n" 2401 "\n| Assets by country | Percent | Current cost |\n", 2402 aSepLine, 2403 ]) 2404 2405 for country in view["analytics"]["distrByCountries"].keys(): 2406 if view["analytics"]["distrByCountries"][country]["cost"] > 0: 2407 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2408 country, 2409 "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]), 2410 "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]), 2411 )) 2412 2413 if details in ["full", "calendar"]: 2414 # -- Show bonds payment calendar section: 2415 if view["stat"]["Bonds"]: 2416 bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]] 2417 view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False) 2418 info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False)) 2419 2420 else: 2421 info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n") 2422 2423 infoText = "".join(info) 2424 2425 if show and not onlyFiles: 2426 uLogger.info(infoText) 2427 2428 if details == "full" and self.overviewFile: 2429 filename = self.overviewFile 2430 2431 elif details == "digest" and self.overviewDigestFile: 2432 filename = self.overviewDigestFile 2433 2434 elif details == "positions" and self.overviewPositionsFile: 2435 filename = self.overviewPositionsFile 2436 2437 elif details == "orders" and self.overviewOrdersFile: 2438 filename = self.overviewOrdersFile 2439 2440 elif details == "analytics" and self.overviewAnalyticsFile: 2441 filename = self.overviewAnalyticsFile 2442 2443 elif details == "calendar" and self.overviewBondsCalendarFile: 2444 filename = self.overviewBondsCalendarFile 2445 2446 else: 2447 filename = "" 2448 2449 if filename and (show or onlyFiles): 2450 with open(filename, "w", encoding="UTF-8") as fH: 2451 fH.write(infoText) 2452 2453 uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename))) 2454 2455 if self.useHTMLReports: 2456 htmlFilePath = filename.replace(".md", ".html") if filename.endswith(".md") else filename + ".html" 2457 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 2458 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's portfolio", commonCSS=COMMON_CSS, markdown=infoText)) 2459 2460 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 2461 2462 return view 2463 2464 def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True, onlyFiles=False) -> tuple[list[dict], dict]: 2465 """ 2466 Returns history operations between two given dates for current `accountId`. 2467 If `reportFile` string is not empty then also save human-readable report. 2468 Shows some statistical data of closed positions. 2469 2470 :param start: see docstring in `TradeRoutines.GetDatesAsString()` method. 2471 :param end: see docstring in `TradeRoutines.GetDatesAsString()` method. 2472 :param show: if `True` then also prints all records to the console. 2473 :param showCancelled: if `False` then remove information about cancelled operations from the deals report. 2474 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 2475 :return: original list of dictionaries with history of deals records from API ("operations" key): 2476 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2477 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc. 2478 """ 2479 if self.accountId is None or not self.accountId: 2480 uLogger.error("Variable `accountId` must be defined for using this method!") 2481 raise Exception("Account ID required") 2482 2483 startDate, endDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT) # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2484 2485 uLogger.debug("Requesting history of a client's operations. Wait, please...") 2486 2487 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2488 dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations" 2489 self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate}) 2490 ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"] # list of dict: operations returns by broker 2491 customStat = {} # custom statistics in additional to responseJSON 2492 2493 # --- output report in human-readable format: 2494 if self.reportFile and (show or onlyFiles): 2495 splitLine1 = "| | | | | |\n" # Summary section 2496 splitLine2 = "| | | | | | | | |\n" # Operations section 2497 nextDay = "" 2498 2499 info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])] 2500 2501 if len(ops) > 0: 2502 customStat = { 2503 "opsCount": 0, # total operations count 2504 "buyCount": 0, # buy operations 2505 "sellCount": 0, # sell operations 2506 "buyTotal": {"rub": 0.}, # Buy sums in different currencies 2507 "sellTotal": {"rub": 0.}, # Sell sums in different currencies 2508 "payIn": {"rub": 0.}, # Deposit brokerage account 2509 "payOut": {"rub": 0.}, # Withdrawals 2510 "divs": {"rub": 0.}, # Dividends income 2511 "coupons": {"rub": 0.}, # Coupon's income 2512 "brokerCom": {"rub": 0.}, # Service commissions 2513 "serviceCom": {"rub": 0.}, # Service commissions 2514 "marginCom": {"rub": 0.}, # Margin commissions 2515 "allTaxes": {"rub": 0.}, # Sum of withholding taxes and corrections 2516 } 2517 2518 # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES: 2519 for item in ops: 2520 if item["state"] == "OPERATION_STATE_EXECUTED": 2521 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2522 2523 # count buy operations: 2524 if "_BUY" in item["operationType"]: 2525 customStat["buyCount"] += 1 2526 2527 if item["payment"]["currency"] in customStat["buyTotal"].keys(): 2528 customStat["buyTotal"][item["payment"]["currency"]] += payment 2529 2530 else: 2531 customStat["buyTotal"][item["payment"]["currency"]] = payment 2532 2533 # count sell operations: 2534 elif "_SELL" in item["operationType"]: 2535 customStat["sellCount"] += 1 2536 2537 if item["payment"]["currency"] in customStat["sellTotal"].keys(): 2538 customStat["sellTotal"][item["payment"]["currency"]] += payment 2539 2540 else: 2541 customStat["sellTotal"][item["payment"]["currency"]] = payment 2542 2543 # count incoming operations: 2544 elif item["operationType"] in ["OPERATION_TYPE_INPUT"]: 2545 if item["payment"]["currency"] in customStat["payIn"].keys(): 2546 customStat["payIn"][item["payment"]["currency"]] += payment 2547 2548 else: 2549 customStat["payIn"][item["payment"]["currency"]] = payment 2550 2551 # count withdrawals operations: 2552 elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]: 2553 if item["payment"]["currency"] in customStat["payOut"].keys(): 2554 customStat["payOut"][item["payment"]["currency"]] += payment 2555 2556 else: 2557 customStat["payOut"][item["payment"]["currency"]] = payment 2558 2559 # count dividends income: 2560 elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]: 2561 if item["payment"]["currency"] in customStat["divs"].keys(): 2562 customStat["divs"][item["payment"]["currency"]] += payment 2563 2564 else: 2565 customStat["divs"][item["payment"]["currency"]] = payment 2566 2567 # count coupon's income: 2568 elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]: 2569 if item["payment"]["currency"] in customStat["coupons"].keys(): 2570 customStat["coupons"][item["payment"]["currency"]] += payment 2571 2572 else: 2573 customStat["coupons"][item["payment"]["currency"]] = payment 2574 2575 # count broker commissions: 2576 elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]: 2577 if item["payment"]["currency"] in customStat["brokerCom"].keys(): 2578 customStat["brokerCom"][item["payment"]["currency"]] += payment 2579 2580 else: 2581 customStat["brokerCom"][item["payment"]["currency"]] = payment 2582 2583 # count service commissions: 2584 elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]: 2585 if item["payment"]["currency"] in customStat["serviceCom"].keys(): 2586 customStat["serviceCom"][item["payment"]["currency"]] += payment 2587 2588 else: 2589 customStat["serviceCom"][item["payment"]["currency"]] = payment 2590 2591 # count margin commissions: 2592 elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]: 2593 if item["payment"]["currency"] in customStat["marginCom"].keys(): 2594 customStat["marginCom"][item["payment"]["currency"]] += payment 2595 2596 else: 2597 customStat["marginCom"][item["payment"]["currency"]] = payment 2598 2599 # count withholding taxes: 2600 elif "_TAX" in item["operationType"]: 2601 if item["payment"]["currency"] in customStat["allTaxes"].keys(): 2602 customStat["allTaxes"][item["payment"]["currency"]] += payment 2603 2604 else: 2605 customStat["allTaxes"][item["payment"]["currency"]] = payment 2606 2607 else: 2608 continue 2609 2610 customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"] 2611 2612 # --- view "Actions" lines: 2613 info.extend([ 2614 "| Report sections | | | | |\n", 2615 "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n", 2616 "| **Actions:** | Trades: {:<21} | Trading volumes: | | |\n".format(customStat["opsCount"]), 2617 "| | Buy: {:<22} | {:<28} | | |\n".format( 2618 "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2619 " rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else " —", 2620 ), 2621 "| | Sell: {:<21} | {:<28} | | |\n".format( 2622 "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2623 " rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else " —", 2624 ), 2625 ]) 2626 2627 opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys())))) 2628 for key in opsKeys: 2629 if key == "rub": 2630 continue 2631 2632 info.extend([ 2633 "| | | {:<28} | | |\n".format( 2634 " {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0) 2635 ), 2636 "| | | {:<28} | | |\n".format( 2637 " {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0) 2638 ), 2639 ]) 2640 2641 info.append(splitLine1) 2642 2643 def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str: 2644 return "| | {:<29} | {:<28} | {:<20} | {:<22} |\n".format( 2645 " {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else " —", 2646 " {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else " —", 2647 " {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else " —", 2648 " {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else " —", 2649 ) 2650 2651 # --- view "Payments" lines: 2652 info.append("| **Payments:** | Deposit on broker account: | Withdrawals: | Dividends income: | Coupons income: |\n") 2653 paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys())))) 2654 2655 for key in paymentsKeys: 2656 info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key)) 2657 2658 info.append(splitLine1) 2659 2660 # --- view "Commissions and taxes" lines: 2661 info.append("| **Commissions and taxes:** | Broker commissions: | Service commissions: | Margin commissions: | All taxes/corrections: |\n") 2662 comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys())))) 2663 2664 for key in comKeys: 2665 info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key)) 2666 2667 info.extend([ 2668 "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"), 2669 "| Date and time | FIGI | Ticker | Asset | Value | Payment | Status | Operation type |\n", 2670 "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n", 2671 ]) 2672 2673 else: 2674 info.append("Broker returned no operations during this period\n") 2675 2676 # --- view "Operations" section: 2677 for item in ops: 2678 if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]: 2679 continue 2680 2681 else: 2682 self._figi = item["figi"] 2683 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2684 instrument = self.SearchByFIGI(requestPrice=False) if self._figi else {} 2685 2686 # group of deals during one day: 2687 if nextDay and item["date"].split("T")[0] != nextDay: 2688 info.append(splitLine2) 2689 nextDay = "" 2690 2691 else: 2692 nextDay = item["date"].split("T")[0] # saving current day for splitting 2693 2694 info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format( 2695 item["date"].replace("T", " ").replace("Z", "").split(".")[0], 2696 self._figi if self._figi else "—", 2697 instrument["ticker"] if instrument else "—", 2698 instrument["type"] if instrument else "—", 2699 item["quantity"] if int(item["quantity"]) > 0 else "—", 2700 "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—", 2701 TKS_OPERATION_STATES[item["state"]], 2702 TKS_OPERATION_TYPES[item["operationType"]], 2703 )) 2704 2705 infoText = "".join(info) 2706 2707 if show and not onlyFiles: 2708 if self.moreDebug: 2709 uLogger.debug("Records about history of a client's operations successfully received") 2710 2711 uLogger.info(infoText) 2712 2713 if self.reportFile and (show or onlyFiles): 2714 with open(self.reportFile, "w", encoding="UTF-8") as fH: 2715 fH.write(infoText) 2716 2717 uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile))) 2718 2719 if self.useHTMLReports: 2720 htmlFilePath = self.reportFile.replace(".md", ".html") if self.reportFile.endswith(".md") else self.reportFile + ".html" 2721 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 2722 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's operations", commonCSS=COMMON_CSS, markdown=infoText)) 2723 2724 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 2725 2726 return ops, customStat 2727 2728 def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False, onlyFiles=False) -> pd.DataFrame: 2729 """ 2730 This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id). 2731 2732 History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. 2733 Warning! Broker server used ISO UTC time by default. 2734 2735 If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame. 2736 Also, `historyFile` used to update history with `onlyMissing` parameter. 2737 2738 See also: `LoadHistory()` and `ShowHistoryChart()` methods. 2739 2740 :param start: see docstring in `TradeRoutines.GetDatesAsString()` method. 2741 :param end: see docstring in `TradeRoutines.GetDatesAsString()` method. 2742 :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`, 2743 `"hour"`, `"day"`. Default: `"hour"`. 2744 :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`. 2745 False by default. Warning! History appends only from last candle to current time 2746 with always update last candle! 2747 :param csvSep: separator if csv-file is used, `,` by default. 2748 :param show: if `True` then also prints Pandas DataFrame to the console. 2749 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 2750 :return: Pandas DataFrame with prices history. Headers of columns are defined by default: 2751 `["date", "time", "open", "high", "low", "close", "volume"]`. 2752 """ 2753 strStartDate, strEndDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT) # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2754 headers = ["date", "time", "open", "high", "low", "close", "volume"] # sequence and names of column headers 2755 history = None # empty pandas object for history 2756 2757 if interval not in TKS_CANDLE_INTERVALS.keys(): 2758 uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.") 2759 raise Exception("Incorrect value") 2760 2761 if not (self._ticker or self._figi): 2762 uLogger.error("Ticker or FIGI must be defined!") 2763 raise Exception("Ticker or FIGI required") 2764 2765 if self._ticker and not self._figi: 2766 instrumentByTicker = self.SearchByTicker(requestPrice=False) 2767 self._figi = instrumentByTicker["figi"] if instrumentByTicker else "" 2768 2769 if self._figi and not self._ticker: 2770 instrumentByFIGI = self.SearchByFIGI(requestPrice=False) 2771 self._ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else "" 2772 2773 dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from start time string 2774 dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from end time string 2775 if interval.lower() != "day": 2776 dtEnd += timedelta(seconds=1) # adds 1 sec for requests, because day end returned by `TradeRoutines.GetDatesAsString()` is 23:59:59 2777 2778 delta = dtEnd - dtStart # current UTC time minus last time in file 2779 deltaMinutes = delta.days * 1440 + delta.seconds // 60 # minutes between start and end dates 2780 2781 # calculate history length in candles: 2782 length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1] 2783 if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0: 2784 length += 1 # to avoid fraction time 2785 2786 # calculate data blocks count: 2787 blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2] 2788 2789 uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self._ticker, self._figi)) 2790 if self.moreDebug: 2791 uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end)) 2792 uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate)) 2793 uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval)) 2794 uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2])) 2795 2796 tempOld = None # pandas object for old history, if --only-missing key present 2797 lastTime = None # datetime object of last old candle in file 2798 2799 if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile): 2800 if self.moreDebug: 2801 uLogger.debug("--only-missing key present, add only last missing candles...") 2802 uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile))) 2803 2804 tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers) 2805 2806 tempOld["date"] = pd.to_datetime(tempOld["date"]) # load date "as is" 2807 tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d") # convert date to string 2808 tempOld["time"] = pd.to_datetime(tempOld["time"]) # load time "as is" 2809 tempOld["time"] = tempOld["time"].dt.strftime("%H:%M") # convert time to string 2810 2811 # get last datetime object from last string in file or minus 1 delta if file is empty: 2812 if len(tempOld) > 0: 2813 lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2814 2815 else: 2816 lastTime = dtEnd - timedelta(days=1) # history file is empty, so last date set at -1 day 2817 2818 tempOld = tempOld[:-1] # always remove last old candle because it may be incompletely at the current time 2819 2820 responseJSONs = [] # raw history blocks of data 2821 2822 blockEnd = dtEnd 2823 for item in range(blocks): 2824 tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2] 2825 blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail) 2826 2827 uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format( 2828 item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2829 )) 2830 2831 if blockStart == blockEnd: 2832 uLogger.debug("Skipped this zero-length block...") 2833 2834 else: 2835 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles 2836 historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles" 2837 self.body = str({ 2838 "figi": self._figi, 2839 "from": blockStart.strftime(TKS_DATE_TIME_FORMAT), 2840 "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2841 "interval": TKS_CANDLE_INTERVALS[interval][0] 2842 }) 2843 responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1) 2844 2845 if "code" in responseJSON.keys(): 2846 uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks)) 2847 2848 else: 2849 if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1: 2850 responseJSON["candles"] = responseJSON["candles"][:-1] # removes last candle for "yesterday" request 2851 2852 responseJSONs = responseJSON["candles"] + responseJSONs # add more old history behind newest dates 2853 2854 blockEnd = blockStart 2855 2856 printCount = len(responseJSONs) # candles to show in console 2857 if responseJSONs: 2858 tempHistory = pd.DataFrame( 2859 data={ 2860 "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2861 "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2862 "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs], 2863 "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs], 2864 "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs], 2865 "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs], 2866 "volume": [int(item["volume"]) for item in responseJSONs], 2867 }, 2868 index=range(len(responseJSONs)), 2869 columns=["date", "time", "open", "high", "low", "close", "volume"], 2870 ) 2871 tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d") 2872 tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M") 2873 2874 # append only newest candles to old history if --only-missing key present: 2875 if onlyMissing and tempOld is not None and lastTime is not None: 2876 index = 0 # find start index in tempHistory data: 2877 2878 for i, item in tempHistory.iterrows(): 2879 curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2880 2881 if curTime == lastTime: 2882 uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 2883 index = i 2884 printCount = index + 1 2885 break 2886 2887 history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True) 2888 2889 else: 2890 history = tempHistory # if no `--only-missing` key then load full data from server 2891 2892 if self.moreDebug: 2893 uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False))) 2894 2895 if history is not None and not history.empty: 2896 if show and not onlyFiles: 2897 uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format( 2898 strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]), 2899 pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False), 2900 )) 2901 2902 else: 2903 uLogger.warning("Received an empty candles history!") 2904 2905 if self.historyFile is not None: 2906 if history is not None and not history.empty: 2907 history.to_csv(self.historyFile, sep=csvSep, index=False, header=False) 2908 uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self._ticker, self._figi, interval, os.path.abspath(self.historyFile))) 2909 2910 else: 2911 uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile))) 2912 2913 else: 2914 if self.moreDebug: 2915 uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.") 2916 2917 return history 2918 2919 def LoadHistory(self, filePath: str) -> pd.DataFrame: 2920 """ 2921 Load candles history from csv-file and return Pandas DataFrame object. 2922 2923 See also: `History()` and `ShowHistoryChart()` methods. 2924 2925 :param filePath: path to csv-file to open. 2926 """ 2927 loadedHistory = None # init candles data object 2928 2929 uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...") 2930 2931 if os.path.exists(filePath): 2932 loadedHistory = self.priceModel.LoadFromFile(filePath) # load data and get chain of candles as Pandas DataFrame 2933 2934 tfStr = self.priceModel.FormattedDelta( 2935 self.priceModel.timeframe, 2936 "{days} days {hours}h {minutes}m {seconds}s", 2937 ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta( 2938 self.priceModel.timeframe, 2939 "{hours}h {minutes}m {seconds}s", 2940 ) 2941 2942 if loadedHistory is not None and not loadedHistory.empty: 2943 uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format( 2944 len(loadedHistory), 2945 tfStr, 2946 pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)), 2947 ) 2948 2949 else: 2950 uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath))) 2951 2952 else: 2953 uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath)) 2954 2955 return loadedHistory 2956 2957 def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None: 2958 """ 2959 Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file. 2960 2961 Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart. 2962 Default: `index.html` (both for interact and non-interact candlesticks chart). 2963 2964 See also: `History()` and `LoadHistory()` methods. 2965 2966 :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object. 2967 :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. 2968 See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters 2969 If False then chain of candlesticks will render as not interactive Google Candlestick chart. 2970 See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template 2971 :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to 2972 html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file. 2973 """ 2974 if isinstance(candles, str): 2975 self.priceModel.prices = self.LoadHistory(filePath=candles) # load candles chain from file 2976 self.priceModel.ticker = os.path.basename(candles) # use filename as ticker name in PriceGenerator 2977 2978 elif isinstance(candles, pd.DataFrame): 2979 self.priceModel.prices = candles # set candles chain from variable 2980 self.priceModel.ticker = self._ticker # use current TKSBrokerAPI ticker as ticker name in PriceGenerator 2981 2982 if "datetime" not in candles.columns: 2983 self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True) # PriceGenerator uses "datetime" column with date and time 2984 2985 else: 2986 uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!") 2987 raise Exception("Incorrect value") 2988 2989 self.priceModel.horizon = len(self.priceModel.prices) # use length of candles data as horizon in PriceGenerator 2990 2991 if interact: 2992 uLogger.debug("Rendering interactive candles chart. Wait, please...") 2993 2994 self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2995 2996 else: 2997 uLogger.debug("Rendering non-interactive candles chart. Wait, please...") 2998 2999 self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 3000 3001 uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile))) 3002 3003 def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 3004 """ 3005 Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response. 3006 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 3007 3008 See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`. 3009 3010 :param operation: string "Buy" or "Sell". 3011 :param lots: volume, integer count of lots >= 1. 3012 :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`. 3013 :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`. 3014 :param expDate: string "Undefined" by default or local date in future, 3015 it is a string with format `%Y-%m-%d %H:%M:%S`. 3016 :return: JSON with response from broker server. 3017 """ 3018 if self.accountId is None or not self.accountId: 3019 uLogger.error("Variable `accountId` must be defined for using this method!") 3020 raise Exception("Account ID required") 3021 3022 if operation is None or not operation or operation not in ("Buy", "Sell"): 3023 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 3024 raise Exception("Incorrect value") 3025 3026 if lots is None or lots < 1: 3027 uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.") 3028 lots = 1 3029 3030 if tp is None or tp < 0: 3031 tp = 0 3032 3033 if sl is None or sl < 0: 3034 sl = 0 3035 3036 if expDate is None or not expDate: 3037 expDate = "Undefined" 3038 3039 if not (self._ticker or self._figi): 3040 uLogger.error("Ticker or FIGI must be defined!") 3041 raise Exception("Ticker or FIGI required") 3042 3043 instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True) 3044 self._ticker = instrument["ticker"] 3045 self._figi = instrument["figi"] 3046 3047 uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self._ticker, self._figi, lots, tp, sl, expDate)) 3048 3049 openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3050 self.body = str({ 3051 "figi": self._figi, 3052 "quantity": str(lots), 3053 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3054 "accountId": str(self.accountId), 3055 "orderType": "ORDER_TYPE_MARKET", # see: TKS_ORDER_TYPES 3056 }) 3057 response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0) 3058 3059 if "orderId" in response.keys(): 3060 uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format( 3061 operation, response["orderId"], 3062 self._ticker, self._figi, lots, 3063 NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"], 3064 NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"], 3065 NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"], 3066 )) 3067 3068 if tp > 0: 3069 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate) 3070 3071 if sl > 0: 3072 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate) 3073 3074 else: 3075 uLogger.warning("Not `oK` status received! Market order not executed. See full debug log and try again open order later.") 3076 3077 return response 3078 3079 def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 3080 """ 3081 More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response. 3082 If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter. 3083 3084 See also: `Order()` and `Trade()` docstrings. 3085 3086 :param lots: volume, integer count of lots >= 1. 3087 :param tp: float > 0, take profit price of stop-order. 3088 :param sl: float > 0, stop loss price of stop-order. 3089 :param expDate: it's a local date in future. 3090 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3091 :return: JSON with response from broker server. 3092 """ 3093 return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate) 3094 3095 def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 3096 """ 3097 More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response. 3098 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 3099 3100 See also: `Order()` and `Trade()` docstrings. 3101 3102 :param lots: volume, integer count of lots >= 1. 3103 :param tp: float > 0, take profit price of stop-order. 3104 :param sl: float > 0, stop loss price of stop-order. 3105 :param expDate: it's a local date in the future. 3106 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3107 :return: JSON with response from broker server. 3108 """ 3109 return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate) 3110 3111 def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None: 3112 """ 3113 Close position of given instruments. 3114 3115 :param instruments: list of instruments defined by tickers or FIGIs that must be closed. 3116 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3117 This avoids unnecessary downloading data from the server. 3118 """ 3119 if instruments is None or not instruments: 3120 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 3121 raise Exception("Ticker or FIGI required") 3122 3123 if isinstance(instruments, str): 3124 instruments = [instruments] 3125 3126 uniqueInstruments = self.GetUniqueFIGIs(instruments) 3127 if uniqueInstruments: 3128 if portfolio is None or not portfolio: 3129 portfolio = self.Overview(show=False) 3130 3131 allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]] 3132 uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened))) 3133 3134 for self._figi in uniqueInstruments: 3135 if self._figi not in allOpened: 3136 uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self._figi)) 3137 continue 3138 3139 # search open trade info about instrument by ticker: 3140 instrument = {} 3141 for iType in TKS_INSTRUMENTS: 3142 if instrument: 3143 break 3144 3145 for item in portfolio["stat"][iType]: 3146 if item["figi"] == self._figi: 3147 instrument = item 3148 break 3149 3150 if instrument: 3151 self._ticker = instrument["ticker"] 3152 self._figi = instrument["figi"] 3153 3154 uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format( 3155 self._ticker, 3156 self._figi, 3157 int(instrument["volume"]), 3158 ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "", 3159 )) 3160 3161 tradeLots = abs(instrument["lots"]) - instrument["blocked"] # available volumes in lots for close operation 3162 3163 if tradeLots > 0: 3164 if instrument["blocked"] > 0: 3165 uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format( 3166 instrument["blocked"], 3167 self._ticker, 3168 tradeLots, 3169 )) 3170 3171 # if direction is "Long" then we need sell, if direction is "Short" then we need buy: 3172 self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots) 3173 3174 else: 3175 uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self._ticker)) 3176 3177 def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None: 3178 """ 3179 Close all positions of given instruments with defined type. 3180 3181 :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list. 3182 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3183 This avoids unnecessary downloading data from the server. 3184 """ 3185 if iType not in TKS_INSTRUMENTS: 3186 uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType)) 3187 3188 else: 3189 if portfolio is None or not portfolio: 3190 portfolio = self.Overview(show=False) 3191 3192 tickers = [item["ticker"] for item in portfolio["stat"][iType]] 3193 uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers)) 3194 3195 if tickers and portfolio: 3196 self.CloseTrades(tickers, portfolio) 3197 3198 else: 3199 uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType)) 3200 3201 def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3202 """ 3203 Universal method to create market or limit orders with all available parameters for current `accountId`. 3204 See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`. 3205 3206 If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above 3207 current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day. 3208 3209 Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" 3210 then broker immediately open market order as you can do simple --buy or --sell operations! 3211 3212 If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". 3213 When current price will go up or down to target price value then broker opens a limit order. 3214 Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter. 3215 3216 Only one attempt and no retry for opens order. If network issue occurred you can create new request. 3217 3218 :param operation: string "Buy" or "Sell". 3219 :param orderType: string "Limit" or "Stop". 3220 :param lots: volume, integer count of lots >= 1. 3221 :param targetPrice: target price > 0. This is open trade price for limit order. 3222 :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. 3223 Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order. 3224 :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types 3225 "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3226 Stop loss order always executed by market price. 3227 :param expDate: string "Undefined" by default or local date in future. 3228 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3229 This date is converting to UTC format for server. This parameter only makes sense for stop-order. 3230 A limit order has no expiration date, it lasts until the end of the trading day. 3231 :return: JSON with response from broker server. 3232 """ 3233 if self.accountId is None or not self.accountId: 3234 uLogger.error("Variable `accountId` must be defined for using this method!") 3235 raise Exception("Account ID required") 3236 3237 if operation is None or not operation or operation not in ("Buy", "Sell"): 3238 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 3239 raise Exception("Incorrect value") 3240 3241 if orderType is None or not orderType or orderType not in ("Limit", "Stop"): 3242 uLogger.error("You must define order type only one of them: `Limit` or `Stop`!") 3243 raise Exception("Incorrect value") 3244 3245 if lots is None or lots < 1: 3246 uLogger.error("You must define trade volume > 0: integer count of lots!") 3247 raise Exception("Incorrect value") 3248 3249 if targetPrice is None or targetPrice <= 0: 3250 uLogger.error("Target price for limit-order must be greater than 0!") 3251 raise Exception("Incorrect value") 3252 3253 if limitPrice is None or limitPrice <= 0: 3254 limitPrice = targetPrice 3255 3256 if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"): 3257 stopType = "Limit" 3258 3259 if expDate is None or not expDate: 3260 expDate = "Undefined" 3261 3262 if not (self._ticker or self._figi): 3263 uLogger.error("Tocker or FIGI must be defined!") 3264 raise Exception("Ticker or FIGI required") 3265 3266 response = {} 3267 instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True) 3268 self._ticker = instrument["ticker"] 3269 self._figi = instrument["figi"] 3270 3271 if orderType == "Limit": 3272 uLogger.debug( 3273 "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format( 3274 self._ticker, self._figi, 3275 operation, lots, targetPrice, instrument["currency"], 3276 )) 3277 3278 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3279 self.body = str({ 3280 "figi": self._figi, 3281 "quantity": str(lots), 3282 "price": FloatToNano(targetPrice), 3283 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3284 "accountId": str(self.accountId), 3285 "orderType": "ORDER_TYPE_LIMIT", # see: TKS_ORDER_TYPES 3286 }) 3287 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3288 3289 if "orderId" in response.keys(): 3290 uLogger.info( 3291 "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}]".format( 3292 response["orderId"], self._ticker, self._figi, operation, lots, 3293 "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"], 3294 )) 3295 3296 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3297 if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]: 3298 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format( 3299 targetPrice, instrument["currency"], 3300 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3301 )) 3302 3303 if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]: 3304 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format( 3305 targetPrice, instrument["currency"], 3306 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3307 )) 3308 3309 else: 3310 uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log and try again open order later.") 3311 3312 if orderType == "Stop": 3313 uLogger.debug( 3314 "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format( 3315 self._ticker, self._figi, 3316 operation, lots, 3317 targetPrice, instrument["currency"], 3318 limitPrice, instrument["currency"], 3319 stopType, expDate, 3320 )) 3321 3322 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder" 3323 expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT) 3324 stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT" 3325 3326 body = { 3327 "figi": self._figi, 3328 "quantity": str(lots), 3329 "price": FloatToNano(limitPrice), 3330 "stopPrice": FloatToNano(targetPrice), 3331 "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL", # see: TKS_STOP_ORDER_DIRECTIONS 3332 "accountId": str(self.accountId), 3333 "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL", # see: TKS_STOP_ORDER_EXPIRATION_TYPES 3334 "stopOrderType": stopOrderType, # see: TKS_STOP_ORDER_TYPES 3335 } 3336 3337 if expDateUTC: 3338 body["expireDate"] = expDateUTC 3339 3340 self.body = str(body) 3341 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3342 3343 if "stopOrderId" in response.keys(): 3344 uLogger.info( 3345 "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}], limit price [{} {}], stop-order type [{}] and expiration date [{} UTC]".format( 3346 response["stopOrderId"], self._ticker, self._figi, operation, lots, 3347 "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"], 3348 "{:.4f}".format(limitPrice).rstrip("0").rstrip("."), instrument["currency"], 3349 TKS_STOP_ORDER_TYPES[stopOrderType], 3350 datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"], 3351 )) 3352 3353 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3354 if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3355 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{} {}] is lower than the current price [{} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3356 "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"], 3357 "{:.4f}".format(instrument["currentPrice"]["lastPrice"]).rstrip("0").rstrip("."), instrument["currency"], 3358 )) 3359 3360 if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3361 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{} {}] is higher than the current price [{} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3362 "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"], 3363 "{:.4f}".format(instrument["currentPrice"]["lastPrice"]).rstrip("0").rstrip("."), instrument["currency"], 3364 )) 3365 3366 else: 3367 uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log and try again open order later.") 3368 3369 return response 3370 3371 def BuyLimit(self, lots: int, targetPrice: float) -> dict: 3372 """ 3373 Create pending `Buy` limit-order (below current price). You must specify only 2 parameters: 3374 `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then 3375 broker immediately open `Buy` market order, such as if you do simple `--buy` operation! 3376 See also: `Order()` docstring. 3377 3378 :param lots: volume, integer count of lots >= 1. 3379 :param targetPrice: target price > 0. This is open trade price for limit order. 3380 :return: JSON with response from broker server. 3381 """ 3382 return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice) 3383 3384 def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3385 """ 3386 Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order. 3387 In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3388 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3389 target price value then broker opens a limit order. See also: `Order()` docstring. 3390 3391 :param lots: volume, integer count of lots >= 1. 3392 :param targetPrice: target price > 0. This is trigger price for buy stop-order. 3393 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3394 with price equal to limitPrice, when current price goes to target price of buy stop-order. 3395 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3396 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3397 :param expDate: string "Undefined" by default or local date in future. 3398 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3399 This date is converting to UTC format for server. 3400 :return: JSON with response from broker server. 3401 """ 3402 return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3403 3404 def SellLimit(self, lots: int, targetPrice: float) -> dict: 3405 """ 3406 Create pending `Sell` limit-order (above current price). You must specify only 2 parameters: 3407 `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then 3408 broker immediately open `Sell` market order, such as if you do simple `--sell` operation! 3409 See also: `Order()` docstring. 3410 3411 :param lots: volume, integer count of lots >= 1. 3412 :param targetPrice: target price > 0. This is open trade price for limit order. 3413 :return: JSON with response from broker server. 3414 """ 3415 return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice) 3416 3417 def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3418 """ 3419 Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order. 3420 In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3421 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3422 target price value then broker opens a limit order. See also: `Order()` docstring. 3423 3424 :param lots: volume, integer count of lots >= 1. 3425 :param targetPrice: target price > 0. This is trigger price for sell stop-order. 3426 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3427 with price equal to limitPrice, when current price goes to target price of sell stop-order. 3428 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3429 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3430 :param expDate: string "Undefined" by default or local date in future. 3431 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3432 This date is converting to UTC format for server. 3433 :return: JSON with response from broker server. 3434 """ 3435 return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3436 3437 def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None: 3438 """ 3439 Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`. 3440 3441 :param orderIDs: list of integers with `orderId` or `stopOrderId`. 3442 :param allOrdersIDs: pre-received lists of all active pending limit orders. 3443 This avoids unnecessary downloading data from the server. 3444 :param allStopOrdersIDs: pre-received lists of all active stop orders. 3445 """ 3446 if self.accountId is None or not self.accountId: 3447 uLogger.error("Variable `accountId` must be defined for using this method!") 3448 raise Exception("Account ID required") 3449 3450 if orderIDs: 3451 if allOrdersIDs is None: 3452 rawOrders = self.RequestPendingOrders() 3453 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending limit orders ID 3454 3455 if allStopOrdersIDs is None: 3456 rawStopOrders = self.RequestStopOrders() 3457 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3458 3459 for orderID in orderIDs: 3460 idInPendingOrders = orderID in allOrdersIDs 3461 idInStopOrders = orderID in allStopOrdersIDs 3462 3463 if not (idInPendingOrders or idInStopOrders): 3464 uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID)) 3465 continue 3466 3467 else: 3468 if idInPendingOrders: 3469 uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID)) 3470 3471 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder 3472 self.body = str({"accountId": self.accountId, "orderId": orderID}) 3473 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder" 3474 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3475 3476 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3477 if self.moreDebug: 3478 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3479 3480 uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID)) 3481 3482 else: 3483 uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID)) 3484 3485 elif idInStopOrders: 3486 uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID)) 3487 3488 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder 3489 self.body = str({"accountId": self.accountId, "stopOrderId": orderID}) 3490 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder" 3491 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3492 3493 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3494 if self.moreDebug: 3495 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3496 3497 uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID)) 3498 3499 else: 3500 uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID)) 3501 3502 else: 3503 continue 3504 3505 def CloseAllOrders(self) -> None: 3506 """ 3507 Gets a list of open pending and stop orders and cancel it all. 3508 """ 3509 rawOrders = self.RequestPendingOrders() 3510 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending limit orders ID 3511 lenOrders = len(allOrdersIDs) 3512 3513 rawStopOrders = self.RequestStopOrders() 3514 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3515 lenSOrders = len(allStopOrdersIDs) 3516 3517 if lenOrders > 0 or lenSOrders > 0: 3518 uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders)) 3519 3520 self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs) 3521 3522 else: 3523 uLogger.info("Orders not found, nothing to cancel.") 3524 3525 def CloseAll(self, *args) -> None: 3526 """ 3527 Close all available (not blocked) opened trades and orders. 3528 3529 Also, you can select one or more keywords case-insensitive: 3530 `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type. 3531 3532 Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods. 3533 """ 3534 overview = self.Overview(show=False) # get all open trades info 3535 3536 if len(args) == 0: 3537 uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...") 3538 self.CloseAllOrders() # close all pending and stop orders 3539 3540 for iType in TKS_INSTRUMENTS: 3541 if iType != "Currencies": 3542 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3543 3544 else: 3545 uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args))) 3546 lowerArgs = [x.lower() for x in args] 3547 3548 if "orders" in lowerArgs: 3549 self.CloseAllOrders() # close all pending and stop orders 3550 3551 for iType in TKS_INSTRUMENTS: 3552 if iType.lower() in lowerArgs and iType != "Currencies": 3553 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3554 3555 def CloseAllByTicker(self, instrument: str) -> None: 3556 """ 3557 Close all available (not blocked) opened trades and orders for one instrument defined by its ticker. 3558 3559 This method searches opened trade and orders of instrument throw all portfolio and then use 3560 `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument. 3561 3562 See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`. 3563 3564 :param instrument: string with ticker. 3565 """ 3566 if instrument is None or not instrument: 3567 uLogger.error("Ticker name must be defined for using this method!") 3568 raise Exception("Ticker required") 3569 3570 overview = self.Overview(show=False) # get user portfolio with all open trades info 3571 3572 self._ticker = instrument # try to set instrument as ticker 3573 self._figi = "" 3574 3575 limitAll = [item["orderID"] for item in overview["stat"]["orders"]] # list of all pending limit order IDs 3576 stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]] # list of all stop order IDs 3577 3578 if limitAll and self.IsInLimitOrders(portfolio=overview): 3579 uLogger.debug("Closing all opened pending limit orders for the instrument with ticker [{}]. Wait, please...") 3580 self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3581 3582 if stopAll and self.IsInStopOrders(portfolio=overview): 3583 uLogger.debug("Closing all opened stop orders for the instrument with ticker [{}]. Wait, please...") 3584 self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3585 3586 if self.IsInPortfolio(portfolio=overview): 3587 uLogger.debug("Closing all available (not blocked) opened trade for the instrument with ticker [{}]. Wait, please...") 3588 self.CloseTrades(instruments=[instrument], portfolio=overview) 3589 3590 def CloseAllByFIGI(self, instrument: str) -> None: 3591 """ 3592 Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id. 3593 3594 This method searches opened trade and orders of instrument throw all portfolio and then use 3595 `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument. 3596 3597 See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`. 3598 3599 :param instrument: string with FIGI id. 3600 """ 3601 if instrument is None or not instrument: 3602 uLogger.error("FIGI id must be defined for using this method!") 3603 raise Exception("FIGI required") 3604 3605 overview = self.Overview(show=False) # get user portfolio with all open trades info 3606 3607 self._ticker = "" 3608 self._figi = instrument # try to set instrument as FIGI id 3609 3610 limitAll = [item["orderID"] for item in overview["stat"]["orders"]] # list of all pending limit order IDs 3611 stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]] # list of all stop order IDs 3612 3613 if limitAll and self.IsInLimitOrders(portfolio=overview): 3614 uLogger.debug("Closing all opened pending limit orders for the instrument with FIGI [{}]. Wait, please...") 3615 self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3616 3617 if stopAll and self.IsInStopOrders(portfolio=overview): 3618 uLogger.debug("Closing all opened stop orders for the instrument with FIGI [{}]. Wait, please...") 3619 self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3620 3621 if self.IsInPortfolio(portfolio=overview): 3622 uLogger.debug("Closing all available (not blocked) opened trade for the instrument with FIGI [{}]. Wait, please...") 3623 self.CloseTrades(instruments=[instrument], portfolio=overview) 3624 3625 @staticmethod 3626 def ParseOrderParameters(operation, **inputParameters): 3627 """ 3628 Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders. 3629 3630 :param operation: string "Buy" or "Sell". 3631 :param inputParameters: this is dict of strings that looks like this 3632 `{"lots": "L_int,...", "prices": "P_float,..."}` where 3633 "lots" key: one or more lot values (integer numbers) to open with every limit-order 3634 "prices" key: one or more prices to open limit-orders 3635 Counts of values in lots and prices lists must be equals! 3636 :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]` 3637 """ 3638 # TODO: update order grid work with api v2 3639 pass 3640 # uLogger.debug("Input parameters: {}".format(inputParameters)) 3641 # 3642 # if operation is None or not operation or operation not in ("Buy", "Sell"): 3643 # uLogger.error("You must define operation type: 'Buy' or 'Sell'!") 3644 # raise Exception("Incorrect value") 3645 # 3646 # if "l" in inputParameters.keys(): 3647 # inputParameters["lots"] = inputParameters.pop("l") 3648 # 3649 # if "p" in inputParameters.keys(): 3650 # inputParameters["prices"] = inputParameters.pop("p") 3651 # 3652 # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys(): 3653 # uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!") 3654 # raise Exception("Incorrect value") 3655 # 3656 # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")] 3657 # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")] 3658 # 3659 # if len(lots) != len(prices): 3660 # uLogger.error("'lots' and 'prices' lists must have equal length of values!") 3661 # raise Exception("Incorrect value") 3662 # 3663 # uLogger.debug("Extracted parameters for orders:") 3664 # uLogger.debug("lots = {}".format(lots)) 3665 # uLogger.debug("prices = {}".format(prices)) 3666 # 3667 # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...] 3668 # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))] 3669 # uLogger.debug("Order parameters: {}".format(result)) 3670 # 3671 # return result 3672 3673 def IsInPortfolio(self, portfolio: dict = None) -> bool: 3674 """ 3675 Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`. 3676 3677 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3678 :return: `True` if portfolio contains open position with given instrument, `False` otherwise. 3679 """ 3680 result = False 3681 msg = "Instrument not defined!" 3682 3683 if portfolio is None or not portfolio: 3684 portfolio = self.Overview(show=False) 3685 3686 if self._ticker: 3687 uLogger.debug("Searching instrument with ticker [{}] throw opened positions list...".format(self._ticker)) 3688 msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker) 3689 3690 for iType in TKS_INSTRUMENTS: 3691 for instrument in portfolio["stat"][iType]: 3692 if instrument["ticker"] == self._ticker: 3693 result = True 3694 msg = "Instrument with ticker [{}] is present in open positions".format(self._ticker) 3695 break 3696 3697 elif self._figi: 3698 uLogger.debug("Searching instrument with FIGI [{}] throw opened positions list...".format(self._figi)) 3699 msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi) 3700 3701 for iType in TKS_INSTRUMENTS: 3702 for instrument in portfolio["stat"][iType]: 3703 if instrument["figi"] == self._figi: 3704 result = True 3705 msg = "Instrument with FIGI [{}] is present in open positions".format(self._figi) 3706 break 3707 3708 else: 3709 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3710 3711 uLogger.debug(msg) 3712 3713 return result 3714 3715 def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict: 3716 """ 3717 Returns instrument from the user's portfolio if it presents there. 3718 Instrument must be defined by `ticker` (highly priority) or `figi`. 3719 3720 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3721 :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise. 3722 """ 3723 result = None 3724 msg = "Instrument not defined!" 3725 3726 if portfolio is None or not portfolio: 3727 portfolio = self.Overview(show=False) 3728 3729 if self._ticker: 3730 uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self._ticker)) 3731 msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker) 3732 3733 for iType in TKS_INSTRUMENTS: 3734 for instrument in portfolio["stat"][iType]: 3735 if instrument["ticker"] == self._ticker: 3736 result = instrument 3737 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self._ticker, instrument["figi"]) 3738 break 3739 3740 elif self._figi: 3741 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self._figi)) 3742 msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi) 3743 3744 for iType in TKS_INSTRUMENTS: 3745 for instrument in portfolio["stat"][iType]: 3746 if instrument["figi"] == self._figi: 3747 result = instrument 3748 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self._figi) 3749 break 3750 3751 else: 3752 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3753 3754 uLogger.debug(msg) 3755 3756 return result 3757 3758 def IsInLimitOrders(self, portfolio: dict = None) -> bool: 3759 """ 3760 Checks if instrument is in the limit orders list. Instrument must be defined by `ticker` (highly priority) or `figi`. 3761 3762 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3763 3764 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3765 :return: `True` if limit orders list contains some limit orders for the instrument, `False` otherwise. 3766 """ 3767 result = False 3768 msg = "Instrument not defined!" 3769 3770 if portfolio is None or not portfolio: 3771 portfolio = self.Overview(show=False) 3772 3773 if self._ticker: 3774 uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker)) 3775 msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker) 3776 3777 for instrument in portfolio["stat"]["orders"]: 3778 if instrument["ticker"] == self._ticker: 3779 result = True 3780 msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker) 3781 break 3782 3783 elif self._figi: 3784 uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi)) 3785 msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi) 3786 3787 for instrument in portfolio["stat"]["orders"]: 3788 if instrument["figi"] == self._figi: 3789 result = True 3790 msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi) 3791 break 3792 3793 else: 3794 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3795 3796 uLogger.debug(msg) 3797 3798 return result 3799 3800 def GetLimitOrderIDs(self, portfolio: dict = None) -> list[str]: 3801 """ 3802 Returns list with all `orderID`s of opened pending limit orders for the instrument. 3803 Instrument must be defined by `ticker` (highly priority) or `figi`. 3804 3805 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3806 3807 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3808 :return: list with `orderID`s of limit orders. 3809 """ 3810 result = [] 3811 msg = "Instrument not defined!" 3812 3813 if portfolio is None or not portfolio: 3814 portfolio = self.Overview(show=False) 3815 3816 if self._ticker: 3817 uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker)) 3818 msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker) 3819 3820 for instrument in portfolio["stat"]["orders"]: 3821 if instrument["ticker"] == self._ticker: 3822 result.append(instrument["orderID"]) 3823 3824 if result: 3825 msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker) 3826 3827 elif self._figi: 3828 uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi)) 3829 msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi) 3830 3831 for instrument in portfolio["stat"]["orders"]: 3832 if instrument["figi"] == self._figi: 3833 result.append(instrument["orderID"]) 3834 3835 if result: 3836 msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi) 3837 3838 else: 3839 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3840 3841 uLogger.debug(msg) 3842 3843 return result 3844 3845 def IsInStopOrders(self, portfolio: dict = None) -> bool: 3846 """ 3847 Checks if instrument is in the stop orders list. Instrument must be defined by `ticker` (highly priority) or `figi`. 3848 3849 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3850 3851 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3852 :return: `True` if stop orders list contains some stop orders for the instrument, `False` otherwise. 3853 """ 3854 result = False 3855 msg = "Instrument not defined!" 3856 3857 if portfolio is None or not portfolio: 3858 portfolio = self.Overview(show=False) 3859 3860 if self._ticker: 3861 uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker)) 3862 msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker) 3863 3864 for instrument in portfolio["stat"]["stopOrders"]: 3865 if instrument["ticker"] == self._ticker: 3866 result = True 3867 msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker) 3868 break 3869 3870 elif self._figi: 3871 uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi)) 3872 msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi) 3873 3874 for instrument in portfolio["stat"]["stopOrders"]: 3875 if instrument["figi"] == self._figi: 3876 result = True 3877 msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi) 3878 break 3879 3880 else: 3881 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3882 3883 uLogger.debug(msg) 3884 3885 return result 3886 3887 def GetStopOrderIDs(self, portfolio: dict = None) -> list[str]: 3888 """ 3889 Returns list with all `orderID`s of opened stop orders for the instrument. 3890 Instrument must be defined by `ticker` (highly priority) or `figi`. 3891 3892 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3893 3894 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3895 :return: list with `orderID`s of stop orders. 3896 """ 3897 result = [] 3898 msg = "Instrument not defined!" 3899 3900 if portfolio is None or not portfolio: 3901 portfolio = self.Overview(show=False) 3902 3903 if self._ticker: 3904 uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker)) 3905 msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker) 3906 3907 for instrument in portfolio["stat"]["stopOrders"]: 3908 if instrument["ticker"] == self._ticker: 3909 result.append(instrument["orderID"]) 3910 3911 if result: 3912 msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker) 3913 3914 elif self._figi: 3915 uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi)) 3916 msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi) 3917 3918 for instrument in portfolio["stat"]["stopOrders"]: 3919 if instrument["figi"] == self._figi: 3920 result.append(instrument["orderID"]) 3921 3922 if result: 3923 msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi) 3924 3925 else: 3926 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3927 3928 uLogger.debug(msg) 3929 3930 return result 3931 3932 def RequestLimits(self) -> dict: 3933 """ 3934 Method for obtaining the available funds for withdrawal for current `accountId`. 3935 3936 See also: 3937 - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits 3938 - `OverviewLimits()` method 3939 3940 :return: dict with raw data from server that contains free funds for withdrawal. Example of dict: 3941 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`. 3942 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency 3943 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures. 3944 """ 3945 if self.accountId is None or not self.accountId: 3946 uLogger.error("Variable `accountId` must be defined for using this method!") 3947 raise Exception("Account ID required") 3948 3949 uLogger.debug("Requesting current available funds for withdrawal. Wait, please...") 3950 3951 self.body = str({"accountId": self.accountId}) 3952 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits" 3953 rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3954 3955 if self.moreDebug: 3956 uLogger.debug("Records about available funds for withdrawal successfully received") 3957 3958 return rawLimits 3959 3960 def OverviewLimits(self, show: bool = False, onlyFiles=False) -> dict: 3961 """ 3962 Method for parsing and show table with available funds for withdrawal for current `accountId`. 3963 3964 See also: `RequestLimits()`. 3965 3966 :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log. 3967 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 3968 :return: dict with raw parsed data from server and some calculated statistics about it. 3969 """ 3970 if self.accountId is None or not self.accountId: 3971 uLogger.error("Variable `accountId` must be defined for using this method!") 3972 raise Exception("Account ID required") 3973 3974 rawLimits = self.RequestLimits() # raw response with current available funds for withdrawal 3975 3976 view = { 3977 "rawLimits": rawLimits, 3978 "limits": { # parsed data for every currency: 3979 "money": { # this is an array of portfolio currency positions 3980 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"] 3981 }, 3982 "blocked": { # this is an array of blocked currency 3983 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"] 3984 }, 3985 "blockedGuarantee": { # this is locked money under collateral for futures 3986 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"] 3987 }, 3988 }, 3989 } 3990 3991 # --- Prepare text table with limits in human-readable format: 3992 if show or onlyFiles: 3993 info = [ 3994 "# Withdrawal limits\n\n", 3995 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 3996 "* **Account ID:** [{}]\n".format(self.accountId), 3997 ] 3998 3999 if view["limits"]["money"]: 4000 info.extend([ 4001 "\n| Currencies | Total | Available for withdrawal | Blocked for trade | Futures guarantee |\n", 4002 "|------------|---------------|--------------------------|-------------------|-------------------|\n", 4003 ]) 4004 4005 else: 4006 info.append("\nNo withdrawal limits\n") 4007 4008 for curr in view["limits"]["money"].keys(): 4009 blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0 4010 blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0 4011 availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee) 4012 4013 infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format( 4014 "[{}]".format(curr), 4015 "{:.2f}".format(view["limits"]["money"][curr]), 4016 "{:.2f}".format(availableMoney), 4017 "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—", 4018 "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—", 4019 ) 4020 4021 if curr == "rub": 4022 info.insert(5, infoStr) # hack: insert "rub" at the first position in table and after headers 4023 4024 else: 4025 info.append(infoStr) 4026 4027 infoText = "".join(info) 4028 4029 if show and not onlyFiles: 4030 uLogger.info(infoText) 4031 4032 if self.withdrawalLimitsFile and (show or onlyFiles): 4033 with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH: 4034 fH.write(infoText) 4035 4036 uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile))) 4037 4038 if self.useHTMLReports: 4039 htmlFilePath = self.withdrawalLimitsFile.replace(".md", ".html") if self.withdrawalLimitsFile.endswith(".md") else self.withdrawalLimitsFile + ".html" 4040 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4041 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Withdrawal limits", commonCSS=COMMON_CSS, markdown=infoText)) 4042 4043 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4044 4045 return view 4046 4047 def RequestAccounts(self) -> dict: 4048 """ 4049 Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`. 4050 4051 See also: 4052 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts 4053 - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account 4054 - `OverviewUserInfo()` method 4055 4056 :return: dict with raw data from server that contains accounts info. Example of dict: 4057 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", 4058 "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", 4059 "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`. 4060 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now. 4061 """ 4062 uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...") 4063 4064 self.body = str({}) 4065 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts" 4066 rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST") 4067 4068 if self.moreDebug: 4069 uLogger.debug("Records about available accounts successfully received") 4070 4071 return rawAccounts 4072 4073 def RequestUserInfo(self) -> dict: 4074 """ 4075 Method for requesting common user's information. 4076 4077 See also: 4078 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo 4079 - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest 4080 - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with 4081 - `OverviewUserInfo()` method 4082 4083 :return: dict with raw data from server that contains user's information. Example of dict: 4084 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", 4085 "russian_shares", "structured_income_bonds"], "tariff": "premium"}`. 4086 """ 4087 uLogger.debug("Requesting common user's information. Wait, please...") 4088 4089 self.body = str({}) 4090 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo" 4091 rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST") 4092 4093 if self.moreDebug: 4094 uLogger.debug("Records about current user successfully received") 4095 4096 return rawUserInfo 4097 4098 def RequestMarginStatus(self, accountId: str = None) -> dict: 4099 """ 4100 Method for requesting margin calculation for defined account ID. 4101 4102 See also: 4103 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes 4104 - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse 4105 - `OverviewUserInfo()` method 4106 4107 :param accountId: string with numeric account ID. If `None`, then used class field `accountId`. 4108 :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. 4109 Example of responses: 4110 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`. 4111 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, 4112 "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, 4113 "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, 4114 "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, 4115 "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`. 4116 """ 4117 if accountId is None or not accountId: 4118 if self.accountId is None or not self.accountId: 4119 uLogger.error("Variable `accountId` must be defined for using this method!") 4120 raise Exception("Account ID required") 4121 4122 else: 4123 accountId = self.accountId # use `self.accountId` (main ID) by default 4124 4125 uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId)) 4126 4127 self.body = str({"accountId": accountId}) 4128 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes" 4129 rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST") 4130 4131 if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}: 4132 uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId)) 4133 rawMargin = {} 4134 4135 else: 4136 if self.moreDebug: 4137 uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId)) 4138 4139 return rawMargin 4140 4141 def RequestTariffLimits(self) -> dict: 4142 """ 4143 Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`. 4144 4145 See also: 4146 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff 4147 - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest 4148 - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit 4149 - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit 4150 - `OverviewUserInfo()` method 4151 4152 :return: dict with raw data from server that contains limits of current tariff. Example of dict: 4153 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], 4154 "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`. 4155 """ 4156 uLogger.debug("Requesting limits of current tariff. Wait, please...") 4157 4158 self.body = str({}) 4159 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff" 4160 rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 4161 4162 if self.moreDebug: 4163 uLogger.debug("Records with limits of current tariff successfully received") 4164 4165 return rawTariffLimits 4166 4167 def RequestBondCoupons(self, iJSON: dict) -> dict: 4168 """ 4169 Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown 4170 then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`. 4171 All dates are in UTC timezone. 4172 4173 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons 4174 Documentation: 4175 - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest 4176 - response: https://tinkoff.github.io/investAPI/instruments/#coupon 4177 4178 See also: `ExtendBondsData()`. 4179 4180 :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self._ticker]` 4181 If raw iJSON is not data of bond then server returns an error [400] with message: 4182 `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`. 4183 :return: dictionary with bond payment calendar. Response example 4184 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", 4185 "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, 4186 "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", 4187 "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}` 4188 """ 4189 if iJSON["figi"] is None or not iJSON["figi"]: 4190 uLogger.error("FIGI must be defined for using this method!") 4191 raise Exception("FIGI required") 4192 4193 startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z" 4194 endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z" 4195 4196 uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format( 4197 "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "", 4198 self._figi, 4199 startDate, 4200 endDate, 4201 )) 4202 4203 self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate}) 4204 calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons" 4205 calendar = self.SendAPIRequest(calendarURL, reqType="POST") 4206 4207 if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}: 4208 uLogger.warning("Instrument type is not bond!") 4209 4210 else: 4211 if self.moreDebug: 4212 uLogger.debug("Records about bond payment calendar successfully received") 4213 4214 return calendar 4215 4216 def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame: 4217 """ 4218 Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider 4219 Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, 4220 coupon yields, current yields and some statistics etc. 4221 4222 WARNING! This is too long operation if a lot of bonds requested from broker server. 4223 4224 See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`. 4225 4226 :param instruments: list of strings with tickers or FIGIs. 4227 :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`, 4228 for further used by data scientists or stock analytics. 4229 :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. 4230 In XLSX-file and Pandas DataFrame fields mean: 4231 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond 4232 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon 4233 """ 4234 if instruments is None or not instruments: 4235 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 4236 raise Exception("Ticker or FIGI required") 4237 4238 if isinstance(instruments, str): 4239 instruments = [instruments] 4240 4241 uniqueInstruments = self.GetUniqueFIGIs(instruments) 4242 4243 uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...") 4244 4245 iCount = len(uniqueInstruments) 4246 tooLong = iCount >= 20 4247 if tooLong: 4248 uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...") 4249 4250 bonds = None 4251 for i, self._figi in enumerate(uniqueInstruments): 4252 instrument = self.SearchByFIGI(requestPrice=False) # raw data about instrument from server 4253 4254 if "type" in instrument.keys() and instrument["type"] == "Bonds": 4255 # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond 4256 rawBond = self.SearchByFIGI(requestPrice=True) 4257 4258 # Widen raw data with UTC current time (iData["actualDateTime"]): 4259 actualDate = datetime.now(tzutc()) 4260 iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond 4261 4262 # Widen raw data with bond payment calendar (iData["rawCalendar"]): 4263 iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)} 4264 4265 # Replace some values with human-readable: 4266 iData["nominalCurrency"] = iData["nominal"]["currency"] 4267 iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"]) 4268 iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"]) 4269 iData["aciCurrency"] = iData["aciValue"]["currency"] 4270 iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"]) 4271 iData["issueSize"] = int(iData["issueSize"]) 4272 iData["issueSizePlan"] = int(iData["issueSizePlan"]) 4273 iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]] 4274 iData["step"] = iData["step"] if "step" in iData.keys() else 0 4275 iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]] 4276 iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0 4277 iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0 4278 iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0 4279 iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0 4280 iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0 4281 iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0 4282 4283 # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date): 4284 iData["limitUpPercent"] = iData["currentPrice"]["limitUp"] # max price on current day in percents of nominal 4285 iData["limitDownPercent"] = iData["currentPrice"]["limitDown"] # min price on current day in percents of nominal 4286 iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"] # last price on market in percents of nominal 4287 iData["closePricePercent"] = iData["currentPrice"]["closePrice"] # previous day close in percents of nominal 4288 iData["changes"] = iData["currentPrice"]["changes"] # this is percent of changes between `currentPrice` and `lastPrice` 4289 iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100 # max price on current day is `limitUpPercent` * `nominal` 4290 iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100 # min price on current day is `limitDownPercent` * `nominal` 4291 iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100 # last price on market is `lastPricePercent` * `nominal` 4292 iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100 # previous day close is `closePricePercent` * `nominal` 4293 iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"] # this is delta between last deal price and last close 4294 4295 # Widen raw data with calendar data from `rawCalendar` values: 4296 calendarData = [] 4297 if "events" in iData["rawCalendar"].keys(): 4298 for item in iData["rawCalendar"]["events"]: 4299 calendarData.append({ 4300 "couponDate": item["couponDate"], 4301 "couponNumber": int(item["couponNumber"]), 4302 "fixDate": item["fixDate"] if "fixDate" in item.keys() else "", 4303 "payCurrency": item["payOneBond"]["currency"], 4304 "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]), 4305 "couponType": TKS_COUPON_TYPES[item["couponType"]], 4306 "couponStartDate": item["couponStartDate"], 4307 "couponEndDate": item["couponEndDate"], 4308 "couponPeriod": item["couponPeriod"], 4309 }) 4310 4311 # if maturity date is unknown then uses the latest date in bond payment calendar for it: 4312 if "maturityDate" not in iData.keys(): 4313 iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else "" 4314 4315 # Widen raw data with Coupon Rate. 4316 # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%: 4317 iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData]) 4318 iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData]) 4319 iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0. 4320 4321 # Widen raw data with Yield to Maturity (YTM) on current date. 4322 # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%: 4323 maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None 4324 iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None 4325 iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate]) 4326 iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"] # sum of all last coupons minus current ACI value 4327 iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0. 4328 4329 iData["calendar"] = calendarData # adds calendar at the end 4330 4331 # Remove not used data: 4332 iData.pop("uid") 4333 iData.pop("positionUid") 4334 iData.pop("currentPrice") 4335 iData.pop("rawCalendar") 4336 4337 colNames = list(iData.keys()) 4338 if bonds is None: 4339 bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames)) 4340 4341 else: 4342 bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True) 4343 4344 else: 4345 uLogger.warning("Instrument is not a bond!") 4346 4347 processed = round(100 * (i + 1) / iCount, 1) 4348 if tooLong and processed % 5 == 0: 4349 uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount)) 4350 4351 else: 4352 uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount)) 4353 4354 bonds.index = bonds["ticker"].tolist() # replace indexes with ticker names 4355 4356 # Saving bonds from Pandas DataFrame to XLSX sheet: 4357 if xlsx and self.bondsXLSXFile: 4358 with pd.ExcelWriter( 4359 path=self.bondsXLSXFile, 4360 date_format=TKS_DATE_FORMAT, 4361 datetime_format=TKS_DATE_TIME_FORMAT, 4362 mode="w", 4363 ) as writer: 4364 bonds.to_excel( 4365 writer, 4366 sheet_name="Extended bonds data", 4367 index=True, 4368 encoding="UTF-8", 4369 freeze_panes=(1, 1), 4370 ) # saving as XLSX-file with freeze first row and column as headers 4371 4372 uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile))) 4373 4374 return bonds 4375 4376 def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame: 4377 """ 4378 Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default. 4379 4380 WARNING! This is too long operation if a lot of bonds requested from broker server. 4381 4382 See also: `ShowBondsCalendar()`, `ExtendBondsData()`. 4383 4384 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4385 extended information about bonds: main info, current prices, bond payment calendar, 4386 coupon yields, current yields and some statistics etc. 4387 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4388 :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default, 4389 for further used by data scientists or stock analytics. 4390 :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon 4391 """ 4392 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4393 extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False) 4394 4395 uLogger.debug("Generating bond payments calendar data. Wait, please...") 4396 4397 colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"] 4398 colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"] 4399 calendar = None 4400 for bond in extBonds.iterrows(): 4401 for item in bond[1]["calendar"]: 4402 cData = { 4403 "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()), 4404 "couponDate": item["couponDate"], 4405 "figi": bond[1]["figi"], 4406 "ticker": bond[1]["ticker"], 4407 "name": bond[1]["name"], 4408 "couponNumber": item["couponNumber"], 4409 "payOneBond": item["payOneBond"], 4410 "payCurrency": item["payCurrency"], 4411 "couponType": item["couponType"], 4412 "couponPeriod": item["couponPeriod"], 4413 "fixDate": item["fixDate"], 4414 "couponStartDate": item["couponStartDate"], 4415 "couponEndDate": item["couponEndDate"], 4416 } 4417 4418 if calendar is None: 4419 calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID)) 4420 4421 else: 4422 calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True) 4423 4424 if calendar is not None: 4425 calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True) # sort all payments for all bonds by payment date 4426 4427 # Saving calendar from Pandas DataFrame to XLSX sheet: 4428 if xlsx: 4429 xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx" 4430 4431 with pd.ExcelWriter( 4432 path=xlsxCalendarFile, 4433 date_format=TKS_DATE_FORMAT, 4434 datetime_format=TKS_DATE_TIME_FORMAT, 4435 mode="w", 4436 ) as writer: 4437 humanReadable = calendar.copy(deep=True) 4438 humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0]) 4439 humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0]) 4440 humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0]) 4441 humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0]) 4442 humanReadable.columns = colNames # human-readable column names 4443 4444 humanReadable.to_excel( 4445 writer, 4446 sheet_name="Bond payments calendar", 4447 index=False, 4448 encoding="UTF-8", 4449 freeze_panes=(1, 2), 4450 ) # saving as XLSX-file with freeze first row and column as headers 4451 4452 del humanReadable # release df in memory 4453 4454 uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile))) 4455 4456 return calendar 4457 4458 def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True, onlyFiles=False) -> str: 4459 """ 4460 Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond. 4461 Also, creates Markdown file with calendar data, `calendar.md` by default. 4462 4463 See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`. 4464 4465 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4466 extended information about bonds: main info, current prices, bond payment calendar, 4467 coupon yields, current yields and some statistics etc. 4468 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4469 :param show: if `True` then also printing bonds payment calendar to the console, 4470 otherwise save to file `calendarFile` only. `False` by default. 4471 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 4472 :return: multilines text in Markdown format with bonds payment calendar as a table. 4473 """ 4474 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4475 extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=show or onlyFiles) 4476 4477 infoText = "# Bond payments calendar\n\n" 4478 4479 calendar = self.CreateBondsCalendar(extBonds, xlsx=show or onlyFiles) # generate Pandas DataFrame with full calendar data 4480 4481 if not (calendar is None or calendar.empty): 4482 splitLine = "| | | | | | | | | |\n" 4483 4484 info = [ 4485 "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4486 "| Paid | Payment date | FIGI | Ticker | No. | Value | Type | Period | End registry date |\n", 4487 "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n", 4488 ] 4489 4490 newMonth = False 4491 notOneBond = calendar["figi"].nunique() > 1 4492 for i, bond in enumerate(calendar.iterrows()): 4493 if newMonth and notOneBond: 4494 info.append(splitLine) 4495 4496 info.append( 4497 "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format( 4498 " √" if bond[1]["paid"] else " —", 4499 bond[1]["couponDate"].split("T")[0], 4500 bond[1]["figi"], 4501 bond[1]["ticker"], 4502 bond[1]["couponNumber"], 4503 "{} {}".format( 4504 "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."), 4505 bond[1]["payCurrency"], 4506 ), 4507 bond[1]["couponType"], 4508 bond[1]["couponPeriod"], 4509 bond[1]["fixDate"].split("T")[0], 4510 ) 4511 ) 4512 4513 if i < len(calendar.values) - 1: 4514 curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4515 nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4516 newMonth = False if curDate.month == nextDate.month else True 4517 4518 else: 4519 newMonth = False 4520 4521 infoText += "".join(info) 4522 4523 if show and not onlyFiles: 4524 uLogger.info("{}".format(infoText)) 4525 4526 if self.calendarFile is not None and (show or onlyFiles): 4527 with open(self.calendarFile, "w", encoding="UTF-8") as fH: 4528 fH.write(infoText) 4529 4530 uLogger.info("Bond payments calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile))) 4531 4532 if self.useHTMLReports: 4533 htmlFilePath = self.calendarFile.replace(".md", ".html") if self.calendarFile.endswith(".md") else self.calendarFile + ".html" 4534 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4535 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Bond payments calendar", commonCSS=COMMON_CSS, markdown=infoText)) 4536 4537 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4538 4539 else: 4540 infoText += "No data\n" 4541 4542 return infoText 4543 4544 def OverviewAccounts(self, show: bool = False, onlyFiles=False) -> dict: 4545 """ 4546 Method for parsing and show simple table with all available user accounts. 4547 4548 See also: `RequestAccounts()` and `OverviewUserInfo()` methods. 4549 4550 :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log. 4551 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 4552 :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict: 4553 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, 4554 "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", 4555 "status": "Opened and active account", "opened": "2018-05-23 00:00:00", 4556 "closed": "—", "access": "Full access" }, ...}}` 4557 """ 4558 rawAccounts = self.RequestAccounts() # Raw responses with accounts 4559 4560 # This is an array of dict with user accounts, its `accountId`s and some parsed data: 4561 accounts = { 4562 item["id"]: { 4563 "type": TKS_ACCOUNT_TYPES[item["type"]], 4564 "name": item["name"], 4565 "status": TKS_ACCOUNT_STATUSES[item["status"]], 4566 "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4567 "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—", 4568 "access": TKS_ACCESS_LEVELS[item["accessLevel"]], 4569 } for item in rawAccounts["accounts"] 4570 } 4571 4572 # Raw and parsed data with some fields replaced in "stat" section: 4573 view = { 4574 "rawAccounts": rawAccounts, 4575 "stat": accounts, 4576 } 4577 4578 # --- Prepare simple text table with only accounts data in human-readable format: 4579 if show or onlyFiles: 4580 info = [ 4581 "# User accounts\n\n", 4582 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4583 "| Account ID | Type | Status | Name |\n", 4584 "|--------------|---------------------------|---------------------------|--------------------------------|\n", 4585 ] 4586 4587 for account in view["stat"].keys(): 4588 info.extend([ 4589 "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format( 4590 account, 4591 view["stat"][account]["type"], 4592 view["stat"][account]["status"], 4593 view["stat"][account]["name"], 4594 ) 4595 ]) 4596 4597 infoText = "".join(info) 4598 4599 if show and not onlyFiles: 4600 uLogger.info(infoText) 4601 4602 if self.userAccountsFile and (show or onlyFiles): 4603 with open(self.userAccountsFile, "w", encoding="UTF-8") as fH: 4604 fH.write(infoText) 4605 4606 uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile))) 4607 4608 if self.useHTMLReports: 4609 htmlFilePath = self.userAccountsFile.replace(".md", ".html") if self.userAccountsFile.endswith(".md") else self.userAccountsFile + ".html" 4610 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4611 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User accounts", commonCSS=COMMON_CSS, markdown=infoText)) 4612 4613 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4614 4615 return view 4616 4617 def OverviewUserInfo(self, show: bool = False, onlyFiles=False) -> dict: 4618 """ 4619 Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). 4620 4621 See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods. 4622 4623 :param show: if `False` then only dictionary returns, if `True` then also print user's data to log. 4624 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 4625 :return: dict with raw parsed data from server and some calculated statistics about it. 4626 """ 4627 overview = self.Overview(show=False) # Request current user portfolio for the ability to calculate missing funds 4628 tmpTicker = self._ticker 4629 self._ticker = "RUB000UTSTOM" # This instrument show in rub how much money cost current margin 4630 missing = self.GetInstrumentFromPortfolio(portfolio=overview) 4631 self._ticker = tmpTicker 4632 4633 rawUserInfo = self.RequestUserInfo() # Raw response with common user info 4634 overviewAccount = self.OverviewAccounts(show=False) # Raw and parsed accounts data 4635 rawAccounts = overviewAccount["rawAccounts"] # Raw response with user accounts data 4636 accounts = overviewAccount["stat"] # Dict with only statistics about user accounts 4637 rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()} # Raw response with margin calculation for every account ID 4638 rawTariffLimits = self.RequestTariffLimits() # Raw response with limits of current tariff 4639 4640 # This is dict with parsed common user data: 4641 userInfo = { 4642 "premium": "Yes" if rawUserInfo["premStatus"] else "No", 4643 "qualified": "Yes" if rawUserInfo["qualStatus"] else "No", 4644 "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]], 4645 "tariff": rawUserInfo["tariff"], 4646 } 4647 4648 # This is an array of dict with parsed margin statuses for every account IDs: 4649 margins = {} 4650 for accountId in accounts.keys(): 4651 if rawMargins[accountId]: 4652 margins[accountId] = { 4653 "currency": rawMargins[accountId]["liquidPortfolio"]["currency"], 4654 "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]), 4655 "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]), 4656 "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]), 4657 "diff": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]), 4658 "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]), 4659 "missing": missing["volume"], 4660 } 4661 4662 else: 4663 margins[accountId] = {} # Server response: margin status is disabled for current accountId 4664 4665 unary = {} # unary-connection limits 4666 for item in rawTariffLimits["unaryLimits"]: 4667 if item["limitPerMinute"] in unary.keys(): 4668 unary[item["limitPerMinute"]].extend(item["methods"]) 4669 4670 else: 4671 unary[item["limitPerMinute"]] = item["methods"] 4672 4673 stream = {} # stream-connection limits 4674 for item in rawTariffLimits["streamLimits"]: 4675 if item["limit"] in stream.keys(): 4676 stream[item["limit"]].extend(item["streams"]) 4677 4678 else: 4679 stream[item["limit"]] = item["streams"] 4680 4681 # This is dict with parsed limits of current tariff (connections, API methods etc.): 4682 limits = { 4683 "unary": unary, 4684 "stream": stream, 4685 } 4686 4687 # Raw and parsed data as an output result: 4688 view = { 4689 "rawUserInfo": rawUserInfo, 4690 "rawAccounts": rawAccounts, 4691 "rawMargins": rawMargins, 4692 "rawTariffLimits": rawTariffLimits, 4693 "stat": { 4694 "overview": overview, 4695 "userInfo": userInfo, 4696 "accounts": accounts, 4697 "margins": margins, 4698 "limits": limits, 4699 }, 4700 } 4701 4702 # --- Prepare text table with user information in human-readable format: 4703 if show or onlyFiles: 4704 info = [ 4705 "# Full user information\n\n", 4706 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4707 "## Common information\n\n", 4708 "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]), 4709 "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]), 4710 "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]), 4711 "* **Allowed to work with instruments:**\n{}\n".format("".join([" - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])), 4712 "\n## User accounts\n\n", 4713 ] 4714 4715 for account in view["stat"]["accounts"].keys(): 4716 info.extend([ 4717 "### ID: [{}]\n\n".format(account), 4718 "| Parameters | Values |\n", 4719 "|----------------------|--------------------------------------------------------------|\n", 4720 "| Account type: | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]), 4721 "| Account name: | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]), 4722 "| Account status: | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]), 4723 "| Access level: | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]), 4724 "| Date opened: | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]), 4725 "| Date closed: | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]), 4726 ]) 4727 4728 if margins[account]: 4729 info.extend([ 4730 "| Margin status: | Enabled |\n", 4731 "| - Liquid portfolio: | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])), 4732 "| - Margin starting: | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])), 4733 "| - Margin minimum: | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])), 4734 "| - Margin difference: | {:<60} |\n".format("{} {}".format(margins[account]["diff"], margins[account]["currency"])), 4735 "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)), 4736 "| - Not covered funds: | {:<60} |\n\n".format("{:.2f} {}".format(margins[account]["missing"], margins[account]["currency"])), 4737 ]) 4738 4739 else: 4740 info.append("| Margin status: | Disabled |\n\n") 4741 4742 info.extend([ 4743 "\n## Current user tariff limits\n", 4744 "\n### See also\n", 4745 "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n", 4746 "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n", 4747 " - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n", 4748 " - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n", 4749 "\n### Unary limits\n", 4750 ]) 4751 4752 if unary: 4753 for key, values in sorted(unary.items()): 4754 info.append("\n* Max requests per minute: {}\n".format(key)) 4755 4756 for value in values: 4757 info.append(" - {}\n".format(value)) 4758 4759 else: 4760 info.append("\nNot available\n") 4761 4762 info.append("\n### Stream limits\n") 4763 4764 if stream: 4765 for key, values in sorted(stream.items()): 4766 info.append("\n* Max stream connections: {}\n".format(key)) 4767 4768 for value in values: 4769 info.append(" - {}\n".format(value)) 4770 4771 else: 4772 info.append("\nNot available\n") 4773 4774 infoText = "".join(info) 4775 4776 if show and not onlyFiles: 4777 uLogger.info(infoText) 4778 4779 if self.userInfoFile and (show or onlyFiles): 4780 with open(self.userInfoFile, "w", encoding="UTF-8") as fH: 4781 fH.write(infoText) 4782 4783 uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile))) 4784 4785 if self.useHTMLReports: 4786 htmlFilePath = self.userInfoFile.replace(".md", ".html") if self.userInfoFile.endswith(".md") else self.userInfoFile + ".html" 4787 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4788 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User info", commonCSS=COMMON_CSS, markdown=infoText)) 4789 4790 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4791 4792 return view 4793 4794 4795class Args: 4796 """ 4797 If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object. 4798 """ 4799 def __init__(self, **kwargs): 4800 self.__dict__.update(kwargs) 4801 4802 def __getattr__(self, item): 4803 return None 4804 4805 4806def ParseArgs(): 4807 """This function get and parse command line keys.""" 4808 parser = ArgumentParser() # command-line string parser 4809 4810 parser.description = "TKSBrokerAPI is a trading platform for automation on Python to simplify the implementation of trading scenarios and work with Tinkoff Invest API server via the REST protocol. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md" 4811 parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]" 4812 4813 # --- options: 4814 4815 parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the platform. `False` by default.") 4816 parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/") 4817 parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.") 4818 4819 parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.") 4820 parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).") 4821 4822 parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.") 4823 parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.") 4824 4825 parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.") 4826 parser.add_argument("--html", "--HTML", action="store_true", default=False, help="Option: if key present then TKSBrokerAPI generate also HTML reports from Markdown. False by default.") 4827 4828 parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.") 4829 parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.") 4830 parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.") 4831 4832 parser.add_argument("--debug-level", "--log-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.") 4833 parser.add_argument("--more", "--more-debug", action="store_true", default=False, help="Option: `--debug-level` key only switch log level verbosity, but in addition `--more` key enable all debug information, such as net request and response headers in all methods.") 4834 parser.add_argument("--tag", type=str, default="", help="Option: identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode. Default: `""` (empty string).") 4835 4836 # --- commands: 4837 4838 parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.") 4839 4840 parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.") 4841 parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.") 4842 parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4843 parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.") 4844 parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!") 4845 parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4846 parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!") 4847 parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.") 4848 4849 parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.") 4850 parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.") 4851 parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.") 4852 parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.") 4853 parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.") 4854 parser.add_argument("--overview-calendar", action="store_true", help="Action: shows only the bonds calendar section (if these present in portfolio). Also, you can define `--output` key to save this information to file, default: `overview-calendar.md`.") 4855 4856 parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.") 4857 parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.") 4858 parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.") 4859 parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).") 4860 4861 parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.") 4862 parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4863 parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4864 4865 parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.") 4866 parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!") 4867 parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!") 4868 parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4869 parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4870 # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4871 # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4872 4873 parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4874 parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4875 parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` (high priority) or `--figi` keys, including for currencies tickers.") 4876 parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers or FIGIs, including for currencies tickers or FIGIs.") 4877 parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations. If the `--close-all` key present with the `--ticker` or `--figi` keys, then positions and all open limit and stop orders for the specified instrument are closed.") 4878 4879 parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.") 4880 parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.") 4881 parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.") 4882 4883 cmdArgs = parser.parse_args() 4884 return cmdArgs 4885 4886 4887def Main(**kwargs): 4888 """ 4889 Main function for work with TKSBrokerAPI in the console. 4890 4891 See examples: 4892 - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md 4893 - in russian: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README.md 4894 """ 4895 args = Args(**kwargs) if kwargs else ParseArgs() # get and parse command-line parameters or use **kwarg parameters 4896 4897 if args.debug_level: 4898 uLogger.level = 10 # always debug level by default 4899 uLogger.handlers[0].level = args.debug_level # level for STDOUT 4900 4901 exitCode = 0 4902 start = datetime.now(tzutc()) 4903 uLogger.debug("=-" * 50) 4904 uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format( 4905 start.strftime(TKS_PRINT_DATE_TIME_FORMAT), 4906 start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4907 )) 4908 4909 # trying to calculate full current version: 4910 buildVersion = __version__ 4911 try: 4912 v = version("tksbrokerapi") 4913 buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0" # set version as major.minor.dev0 if run as local build or local script 4914 4915 except Exception: 4916 buildVersion = __version__ + ".dev0" # if an errors occurred then also set version as major.minor.dev0 4917 4918 uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion)) 4919 uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT)) 4920 4921 try: 4922 if args.version: 4923 print("TKSBrokerAPI {}".format(buildVersion)) 4924 uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion)) 4925 4926 else: 4927 # Init class for trading with Tinkoff Broker: 4928 trader = TinkoffBrokerServer( 4929 token=args.token, 4930 accountId=args.account_id, 4931 useCache=not args.no_cache, 4932 ) 4933 4934 if args.tag is not None: 4935 trader.tag = args.tag # Identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode 4936 4937 # --- set some options: 4938 4939 if args.more: 4940 trader.moreDebug = True 4941 uLogger.warning("More debug info mode is enabled! See network requests, responses and its headers in the full log or run TKSBrokerAPI platform with the `--verbosity 10` to show theres in console.") 4942 4943 if args.html: 4944 trader.useHTMLReports = True 4945 4946 if args.ticker: 4947 ticker = str(args.ticker).upper() # Tickers may be upper case only 4948 4949 if ticker in trader.aliasesKeys: 4950 trader.ticker = trader.aliases[ticker] # Replace some tickers with its aliases 4951 4952 else: 4953 trader.ticker = ticker 4954 4955 if args.figi: 4956 trader.figi = str(args.figi).upper() # FIGIs may be upper case only 4957 4958 if args.depth is not None: 4959 trader.depth = args.depth 4960 4961 # --- do one command: 4962 4963 if args.list: 4964 if args.output is not None: 4965 trader.instrumentsFile = args.output 4966 4967 trader.ShowInstrumentsInfo(show=True) 4968 4969 elif args.list_xlsx: 4970 trader.DumpInstrumentsAsXLSX(forceUpdate=False) 4971 4972 elif args.bonds_xlsx is not None: 4973 if args.output is not None: 4974 trader.bondsXLSXFile = args.output 4975 4976 if len(args.bonds_xlsx) == 0: 4977 trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=True) # request bonds with all available tickers 4978 4979 else: 4980 trader.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True) # request list of given bonds 4981 4982 elif args.search: 4983 if args.output is not None: 4984 trader.searchResultsFile = args.output 4985 4986 trader.SearchInstruments(pattern=args.search[0], show=True) 4987 4988 elif args.info: 4989 if not (args.ticker or args.figi): 4990 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4991 raise Exception("Ticker or FIGI required") 4992 4993 if args.output is not None: 4994 trader.infoFile = args.output 4995 4996 if args.ticker: 4997 trader.SearchByTicker(requestPrice=True, show=True) # show info and current prices by ticker name 4998 4999 else: 5000 trader.SearchByFIGI(requestPrice=True, show=True) # show info and current prices by FIGI id 5001 5002 elif args.calendar is not None: 5003 if args.output is not None: 5004 trader.calendarFile = args.output 5005 5006 if len(args.calendar) == 0: 5007 bondsData = trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=False) # request bonds with all available tickers 5008 5009 else: 5010 bondsData = trader.ExtendBondsData(instruments=args.calendar, xlsx=False) # request list of given bonds 5011 5012 trader.ShowBondsCalendar(extBonds=bondsData, show=True) # shows bonds payment calendar only 5013 5014 elif args.price: 5015 if not (args.ticker or args.figi): 5016 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 5017 raise Exception("Ticker or FIGI required") 5018 5019 trader.GetCurrentPrices(show=True) 5020 5021 elif args.prices is not None: 5022 if args.output is not None: 5023 trader.pricesFile = args.output 5024 5025 trader.GetListOfPrices(instruments=args.prices, show=True) # WARNING: too long wait for a lot of instruments prices 5026 5027 elif args.overview: 5028 if args.output is not None: 5029 trader.overviewFile = args.output 5030 5031 trader.Overview(show=True, details="full") 5032 5033 elif args.overview_digest: 5034 if args.output is not None: 5035 trader.overviewDigestFile = args.output 5036 5037 trader.Overview(show=True, details="digest") 5038 5039 elif args.overview_positions: 5040 if args.output is not None: 5041 trader.overviewPositionsFile = args.output 5042 5043 trader.Overview(show=True, details="positions") 5044 5045 elif args.overview_orders: 5046 if args.output is not None: 5047 trader.overviewOrdersFile = args.output 5048 5049 trader.Overview(show=True, details="orders") 5050 5051 elif args.overview_analytics: 5052 if args.output is not None: 5053 trader.overviewAnalyticsFile = args.output 5054 5055 trader.Overview(show=True, details="analytics") 5056 5057 elif args.overview_calendar: 5058 if args.output is not None: 5059 trader.overviewAnalyticsFile = args.output 5060 5061 trader.Overview(show=True, details="calendar") 5062 5063 elif args.deals is not None: 5064 if args.output is not None: 5065 trader.reportFile = args.output 5066 5067 if 0 <= len(args.deals) < 3: 5068 trader.Deals( 5069 start=args.deals[0] if len(args.deals) >= 1 else None, 5070 end=args.deals[1] if len(args.deals) == 2 else None, 5071 show=True, # Always show deals report in console 5072 showCancelled=not args.no_cancelled, # If --no-cancelled key then remove cancelled operations from the deals report. False by default. 5073 ) 5074 5075 else: 5076 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 5077 raise Exception("Incorrect value") 5078 5079 elif args.history is not None: 5080 if args.output is not None: 5081 trader.historyFile = args.output 5082 5083 if 0 <= len(args.history) < 3: 5084 dataReceived = trader.History( 5085 start=args.history[0] if len(args.history) >= 1 else None, 5086 end=args.history[1] if len(args.history) == 2 else None, 5087 interval="hour" if args.interval is None or not args.interval else args.interval, 5088 onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing, 5089 csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep, 5090 show=True, # shows all downloaded candles in console 5091 ) 5092 5093 if args.render_chart is not None and dataReceived is not None: 5094 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 5095 5096 trader.ShowHistoryChart( 5097 candles=dataReceived, 5098 interact=iChart, 5099 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 5100 ) 5101 5102 else: 5103 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 5104 raise Exception("Incorrect value") 5105 5106 elif args.load_history is not None: 5107 histData = trader.LoadHistory(filePath=args.load_history) # load data from file and show history in console 5108 5109 if args.render_chart is not None and histData is not None: 5110 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 5111 trader.ticker = os.path.basename(args.load_history) # use filename as ticker name for PriceGenerator's chart 5112 5113 trader.ShowHistoryChart( 5114 candles=histData, 5115 interact=iChart, 5116 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 5117 ) 5118 5119 elif args.trade is not None: 5120 if 1 <= len(args.trade) <= 5: 5121 trader.Trade( 5122 operation=args.trade[0], 5123 lots=int(args.trade[1]) if len(args.trade) >= 2 else 1, 5124 tp=float(args.trade[2]) if len(args.trade) >= 3 else 0., 5125 sl=float(args.trade[3]) if len(args.trade) >= 4 else 0., 5126 expDate=args.trade[4] if len(args.trade) == 5 else "Undefined", 5127 ) 5128 5129 else: 5130 uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 5131 5132 elif args.buy is not None: 5133 if 0 <= len(args.buy) <= 4: 5134 trader.Buy( 5135 lots=int(args.buy[0]) if len(args.buy) >= 1 else 1, 5136 tp=float(args.buy[1]) if len(args.buy) >= 2 else 0., 5137 sl=float(args.buy[2]) if len(args.buy) >= 3 else 0., 5138 expDate=args.buy[3] if len(args.buy) == 4 else "Undefined", 5139 ) 5140 5141 else: 5142 uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 5143 5144 elif args.sell is not None: 5145 if 0 <= len(args.sell) <= 4: 5146 trader.Sell( 5147 lots=int(args.sell[0]) if len(args.sell) >= 1 else 1, 5148 tp=float(args.sell[1]) if len(args.sell) >= 2 else 0., 5149 sl=float(args.sell[2]) if len(args.sell) >= 3 else 0., 5150 expDate=args.sell[3] if len(args.sell) == 4 else "Undefined", 5151 ) 5152 5153 else: 5154 uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 5155 5156 elif args.order: 5157 if 4 <= len(args.order) <= 7: 5158 trader.Order( 5159 operation=args.order[0], 5160 orderType=args.order[1], 5161 lots=int(args.order[2]), 5162 targetPrice=float(args.order[3]), 5163 limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0., 5164 stopType=args.order[5] if len(args.order) >= 6 else "Limit", 5165 expDate=args.order[6] if len(args.order) == 7 else "Undefined", 5166 ) 5167 5168 else: 5169 uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`") 5170 5171 elif args.buy_limit: 5172 trader.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1]) 5173 5174 elif args.sell_limit: 5175 trader.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1]) 5176 5177 elif args.buy_stop: 5178 if 2 <= len(args.buy_stop) <= 7: 5179 trader.BuyStop( 5180 lots=int(args.buy_stop[0]), 5181 targetPrice=float(args.buy_stop[1]), 5182 limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0., 5183 stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit", 5184 expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined", 5185 ) 5186 5187 else: 5188 uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 5189 5190 elif args.sell_stop: 5191 if 2 <= len(args.sell_stop) <= 7: 5192 trader.SellStop( 5193 lots=int(args.sell_stop[0]), 5194 targetPrice=float(args.sell_stop[1]), 5195 limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0., 5196 stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit", 5197 expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined", 5198 ) 5199 5200 else: 5201 uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help") 5202 5203 # elif args.buy_order_grid is not None: 5204 # # update order grid work with api v2 5205 # if len(args.buy_order_grid) == 2: 5206 # orderParams = trader.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid)) 5207 # 5208 # for order in orderParams: 5209 # trader.Order(operation="Buy", lots=order["lot"], price=order["price"]) 5210 # 5211 # else: 5212 # uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 5213 # 5214 # elif args.sell_order_grid is not None: 5215 # # update order grid work with api v2 5216 # if len(args.sell_order_grid) >= 2: 5217 # orderParams = trader.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid)) 5218 # 5219 # for order in orderParams: 5220 # trader.Order(operation="Sell", lots=order["lot"], price=order["price"]) 5221 # 5222 # else: 5223 # uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 5224 5225 elif args.close_order is not None: 5226 trader.CloseOrders(args.close_order) # close only one order 5227 5228 elif args.close_orders is not None: 5229 trader.CloseOrders(args.close_orders) # close list of orders 5230 5231 elif args.close_trade: 5232 if not (args.ticker or args.figi): 5233 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 5234 raise Exception("Ticker or FIGI required") 5235 5236 if args.ticker: 5237 trader.CloseTrades([str(args.ticker).upper()]) # close only one trade by ticker (priority) 5238 5239 else: 5240 trader.CloseTrades([str(args.figi).upper()]) # close only one trade by FIGI 5241 5242 elif args.close_trades is not None: 5243 trader.CloseTrades(args.close_trades) # close trades for list of tickers 5244 5245 elif args.close_all is not None: 5246 if args.ticker: 5247 trader.CloseAllByTicker(instrument=str(args.ticker).upper()) 5248 5249 elif args.figi: 5250 trader.CloseAllByFIGI(instrument=str(args.figi).upper()) 5251 5252 else: 5253 trader.CloseAll(*args.close_all) 5254 5255 elif args.limits: 5256 if args.output is not None: 5257 trader.withdrawalLimitsFile = args.output 5258 5259 trader.OverviewLimits(show=True) 5260 5261 elif args.user_info: 5262 if args.output is not None: 5263 trader.userInfoFile = args.output 5264 5265 trader.OverviewUserInfo(show=True) 5266 5267 elif args.account: 5268 if args.output is not None: 5269 trader.userAccountsFile = args.output 5270 5271 trader.OverviewAccounts(show=True) 5272 5273 else: 5274 uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.") 5275 raise Exception("There is no command to execute") 5276 5277 except Exception: 5278 trace = tb.format_exc() 5279 for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]: 5280 if e in trace: 5281 uLogger.error("Check your Internet connection! Failed to establish connection to broker server!") 5282 break 5283 5284 uLogger.debug(trace) 5285 uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues") 5286 exitCode = 255 # an error occurred, must be open a ticket for this issue 5287 5288 finally: 5289 finish = datetime.now(tzutc()) 5290 5291 if exitCode == 0: 5292 if args.more: 5293 uLogger.debug("All operations were finished success (summary code is 0).") 5294 5295 else: 5296 uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format( 5297 os.path.abspath(uLog.defaultLogFile), exitCode, 5298 )) 5299 5300 uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start)) 5301 uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format( 5302 finish.strftime(TKS_PRINT_DATE_TIME_FORMAT), 5303 finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 5304 )) 5305 uLogger.debug("=-" * 50) 5306 5307 if not kwargs: 5308 sys.exit(exitCode) 5309 5310 else: 5311 return exitCode 5312 5313 5314if __name__ == "__main__": 5315 Main()
80class TinkoffBrokerServer: 81 """ 82 This class implements methods to work with Tinkoff broker server. 83 84 Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/ 85 86 About `token`: https://tinkoff.github.io/investAPI/token/ 87 """ 88 def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None: 89 """ 90 Main class init. 91 92 :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`. 93 :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. 94 Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`. 95 :param useCache: use default cache file with raw data to use instead of `iList`. 96 True by default. Cache is auto-update if new day has come. 97 If you don't want to use cache and always updates raw data then set `useCache=False`. 98 :param defaultCache: path to default cache file. `dump.json` by default. 99 """ 100 if token is None or not token: 101 try: 102 self.token = r"{}".format(os.environ["TKS_API_TOKEN"]) 103 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/") 104 105 except KeyError: 106 uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/") 107 raise Exception("Token required") 108 109 else: 110 self.token = token # highly priority than environment variable 'TKS_API_TOKEN' 111 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`") 112 113 if accountId is None or not accountId: 114 try: 115 self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"]) 116 uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId)) 117 118 except KeyError: 119 uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).") 120 121 else: 122 self.accountId = accountId # highly priority than environment variable 'TKS_ACCOUNT_ID' 123 uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId)) 124 125 self.version = __version__ # duplicate here used TKSBrokerAPI main version 126 """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only. 127 128 Latest version: https://pypi.org/project/tksbrokerapi/ 129 """ 130 131 self._tag = "" 132 """Identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode. Default: `""` (empty string).""" 133 134 self.__lock = Lock() # initialize multiprocessing mutex lock 135 136 self._precision = 4 # precision, signs after comma, e.g. 2 for instruments like PLZL, 4 for instruments like USDRUB, if -1 then auto detect it when load data-file 137 138 self.aliases = TKS_TICKER_ALIASES 139 """Some aliases instead official tickers. 140 141 See also: `TKSEnums.TKS_TICKER_ALIASES` 142 """ 143 144 self.aliasesKeys = self.aliases.keys() # re-calc only first time at class init 145 146 self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there 147 148 self._ticker = "" 149 """String with ticker, e.g. `GOOGL`. Tickers may be upper case only. 150 151 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 152 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 153 154 See also: `SearchByTicker()`, `SearchInstruments()`. 155 """ 156 157 self._figi = "" 158 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 159 160 See also: `SearchByFIGI()`, `SearchInstruments()`. 161 """ 162 163 self.depth = 1 164 """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI. 165 166 See also: `GetCurrentPrices()`. 167 """ 168 169 self.server = r"https://invest-public-api.tinkoff.ru/rest" 170 """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest 171 172 See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`. 173 """ 174 175 uLogger.debug("Broker API server: {}".format(self.server)) 176 177 self.timeout = 15 178 """Server operations timeout in seconds. Default: `15`. 179 180 See also: `SendAPIRequest()`. 181 """ 182 183 self.headers = { 184 "Content-Type": "application/json", 185 "accept": "application/json", 186 "Authorization": "Bearer {}".format(self.token), 187 "x-app-name": "Tim55667757.TKSBrokerAPI", 188 } 189 """ 190 Headers which send in every request to broker server. Please, do not change it! 191 Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}", "x-app-name": "Tim55667757.TKSBrokerAPI"}`. 192 193 See also: `SendAPIRequest()`. 194 """ 195 196 self.body = None 197 """Request body which send to broker server. Default: `None`. 198 199 See also: `SendAPIRequest()`. 200 """ 201 202 self.moreDebug = False 203 """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default.""" 204 205 self.useHTMLReports = False 206 """ 207 If `True` then TKSBrokerAPI generate also HTML reports from Markdown. `False` by default. 208 209 See also: Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance. 210 """ 211 212 self.historyFile = None 213 """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame. 214 215 See also: `History()`. 216 """ 217 218 self.htmlHistoryFile = "index.html" 219 """Full path to the html file where rendered candles chart stored. Default: `index.html`. 220 221 See also: `ShowHistoryChart()`. 222 """ 223 224 self.instrumentsFile = "instruments.md" 225 """Filename where full available to user instruments list will be saved. Default: `instruments.md`. 226 227 See also: `ShowInstrumentsInfo()`. 228 """ 229 230 self.searchResultsFile = "search-results.md" 231 """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`. 232 233 See also: `SearchInstruments()`. 234 """ 235 236 self.pricesFile = "prices.md" 237 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 238 239 See also: `GetListOfPrices()`. 240 """ 241 242 self.infoFile = "info.md" 243 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 244 245 See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`. 246 """ 247 248 self.bondsXLSXFile = "ext-bonds.xlsx" 249 """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 250 bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`. 251 252 See also: `ExtendBondsData()`. 253 """ 254 255 self.calendarFile = "calendar.md" 256 """Filename where bonds payment calendar will be saved. Default: `calendar.md`. 257 258 Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`. 259 260 See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`. 261 """ 262 263 self.overviewFile = "overview.md" 264 """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`. 265 266 See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`. 267 """ 268 269 self.overviewDigestFile = "overview-digest.md" 270 """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`. 271 272 See also: `Overview()` with parameter `details="digest"`. 273 """ 274 275 self.overviewPositionsFile = "overview-positions.md" 276 """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`. 277 278 See also: `Overview()` with parameter `details="positions"`. 279 """ 280 281 self.overviewOrdersFile = "overview-orders.md" 282 """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`. 283 284 See also: `Overview()` with parameter `details="orders"`. 285 """ 286 287 self.overviewAnalyticsFile = "overview-analytics.md" 288 """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`. 289 290 See also: `Overview()` with parameter `details="analytics"`. 291 """ 292 293 self.overviewBondsCalendarFile = "overview-calendar.md" 294 """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`. 295 296 See also: `Overview()` with parameter `details="calendar"`. 297 """ 298 299 self.reportFile = "deals.md" 300 """Filename where history of deals and trade statistics will be saved. Default: `deals.md`. 301 302 See also: `Deals()`. 303 """ 304 305 self.withdrawalLimitsFile = "limits.md" 306 """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`. 307 308 See also: `OverviewLimits()` and `RequestLimits()`. 309 """ 310 311 self.userInfoFile = "user-info.md" 312 """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`. 313 314 See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`. 315 """ 316 317 self.userAccountsFile = "accounts.md" 318 """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`. 319 320 See also: `OverviewAccounts()`, `RequestAccounts()`. 321 """ 322 323 self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache 324 """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`. 325 326 Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`. 327 328 See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`. 329 """ 330 331 self.iList = None # init iList for raw instruments data 332 """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`. 333 334 See also: `Listing()`, `DumpInstruments()`. 335 """ 336 337 # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server: 338 if useCache: 339 if os.path.exists(self.iListDumpFile): 340 dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc()) # dump modification date and time 341 curTime = datetime.now(tzutc()) 342 343 if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year): 344 uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 345 346 self.DumpInstruments(forceUpdate=True) # updating self.iList and dump file 347 348 else: 349 self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8")) # load iList from dump 350 351 uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format( 352 os.path.abspath(self.iListDumpFile), 353 dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT), 354 )) 355 356 else: 357 uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...") 358 self.DumpInstruments(forceUpdate=True) # updating self.iList and creating default dump file 359 360 else: 361 self.iList = self.Listing() # request new raw instruments data from broker server 362 self.DumpInstruments(forceUpdate=False) # save raw instrument's data to default dump file `iListDumpFile` 363 364 self.priceModel = PriceGenerator() # init PriceGenerator object to work with candles data 365 """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on. 366 367 See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator 368 """ 369 370 @property 371 def tag(self) -> str: 372 """Identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode. Default: `""` (empty string).""" 373 return self._tag 374 375 @tag.setter 376 def tag(self, value): 377 """Setter for Identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode. Default: `""` (empty string).""" 378 self._tag = str(value) 379 380 if self._tag: 381 for handler in uLogger.handlers: 382 handler.setFormatter(uLog.logging.Formatter(uLog.formatStringWithTag.format(tag=self._tag))) 383 384 uLogger.debug("Custom TKSBrokerAPI tag was set: {}".format(self._tag)) 385 386 else: 387 for handler in uLogger.handlers: 388 handler.setFormatter(uLog.logging.Formatter(uLog.formatString)) 389 390 uLogger.debug("Default logger format is used") 391 392 @property 393 def ticker(self) -> str: 394 """String with ticker, e.g. `GOOGL`. Tickers may be upper case only. 395 396 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 397 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 398 399 See also: `SearchByTicker()`, `SearchInstruments()`. 400 """ 401 return self._ticker 402 403 @ticker.setter 404 def ticker(self, value): 405 """Setter for string with ticker, e.g. `GOOGL`. Tickers may be upper case only. 406 407 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 408 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 409 410 See also: `SearchByTicker()`, `SearchInstruments()`. 411 """ 412 self._ticker = str(value).upper() # Tickers may be upper case only 413 414 @property 415 def figi(self) -> str: 416 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 417 418 See also: `SearchByFIGI()`, `SearchInstruments()`. 419 """ 420 return self._figi 421 422 @figi.setter 423 def figi(self, value): 424 """Setter for string with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 425 426 See also: `SearchByFIGI()`, `SearchInstruments()`. 427 """ 428 self._figi = str(value).upper() # FIGI may be upper case only 429 430 @property 431 def precision(self) -> int: 432 return self._precision 433 434 @precision.setter 435 def precision(self, value): 436 if value >= 0: 437 self._precision = value 438 439 else: 440 self._precision = -1 # auto-detect precision next when data-file load 441 442 def _ParseJSON(self, rawData="{}") -> dict: 443 """ 444 Parse JSON from response string. 445 446 :param rawData: this is a string with JSON-formatted text. 447 :return: JSON (dictionary), parsed from server response string. If an error occurred, then returns empty dict `{}`. 448 """ 449 try: 450 responseJSON = json.loads(rawData) if rawData else {} 451 452 if self.moreDebug: 453 uLogger.debug("JSON formatted raw body data of response:\n{}".format(json.dumps(responseJSON, indent=4))) 454 455 return responseJSON 456 457 except Exception as e: 458 uLogger.error("An empty dict will be return, because an error occurred in `_ParseJSON()` method with comment: {}".format(e)) 459 460 return {} 461 462 def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict: 463 """ 464 Send GET or POST request to broker server and receive JSON object. 465 466 self.header: must be defining with dictionary of headers. 467 self.body: if define then used as request body. None by default. 468 self.timeout: global request timeout, 15 seconds by default. 469 :param url: url with REST request. 470 :param reqType: send "GET" or "POST" request. "GET" by default. 471 :param retry: how many times retry after first request if an 5xx server errors occurred. 472 :param pause: sleep time in seconds between retries. 473 :return: response JSON (dictionary) from broker. 474 """ 475 if reqType.upper() not in ("GET", "POST"): 476 uLogger.error("You can define request type: `GET` or `POST`!") 477 raise Exception("Incorrect value") 478 479 if self.moreDebug: 480 uLogger.debug("Request parameters:") 481 uLogger.debug(" - REST API URL: {}".format(url)) 482 uLogger.debug(" - request type: {}".format(reqType)) 483 uLogger.debug(" - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***"))) 484 uLogger.debug(" - body:\n{}".format(self.body)) 485 486 # fast hack to avoid all operations with some tickers/FIGI 487 responseJSON = {} 488 oK = True 489 for item in self.exclude: 490 if item in url: 491 if self.moreDebug: 492 uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude))) 493 494 oK = False 495 break 496 497 if oK: 498 with self.__lock: # acquire the mutex lock 499 counter = 0 500 response = None 501 errMsg = "" 502 503 while not response and counter <= retry: 504 if reqType == "GET": 505 response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout) 506 507 if reqType == "POST": 508 response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout) 509 510 if self.moreDebug: 511 uLogger.debug("Response:") 512 uLogger.debug(" - status code: {}".format(response.status_code)) 513 uLogger.debug(" - reason: {}".format(response.reason)) 514 uLogger.debug(" - body length: {}".format(len(response.text))) 515 uLogger.debug(" - headers:\n{}".format(response.headers)) 516 517 # Server returns some headers: 518 # - `x-ratelimit-limit` — shows the settings of the current user limit for this method. 519 # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute. 520 # - `x-ratelimit-reset` — time in seconds before resetting the request counter. 521 # See: https://tinkoff.github.io/investAPI/grpc/#kreya 522 if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0": 523 rateLimitWait = int(response.headers["x-ratelimit-reset"]) 524 uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait)) 525 sleep(rateLimitWait) 526 527 # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes 528 if 400 <= response.status_code < 500: 529 msg = "status code: [{}], response body: {}".format(response.status_code, response.text) 530 uLogger.debug(" - not oK, but do not retry for 4xx errors, {}".format(msg)) 531 532 if "code" in response.text and "message" in response.text: 533 msgDict = self._ParseJSON(rawData=response.text) 534 uLogger.warning("HTTP-status code [{}], server message: {}".format(response.status_code, msgDict["message"])) 535 536 counter = retry + 1 # do not retry for 4xx errors 537 538 if 500 <= response.status_code < 600: 539 errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text) 540 uLogger.debug(" - not oK, {}".format(errMsg)) 541 542 if "code" in response.text and "message" in response.text: 543 errMsgDict = self._ParseJSON(rawData=response.text) 544 uLogger.warning("HTTP-status code [{}], error message: {}".format(response.status_code, errMsgDict["message"])) 545 546 counter += 1 547 548 if counter <= retry: 549 uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause)) 550 sleep(pause) 551 552 responseJSON = self._ParseJSON(rawData=response.text) 553 554 if errMsg: 555 uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/") 556 uLogger.error(" - not oK, {}".format(errMsg)) 557 558 return responseJSON 559 560 def _IUpdater(self, iType: str) -> tuple: 561 """ 562 Request instrument by type from server. See available API methods for instruments: 563 Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies 564 Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares 565 Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds 566 Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs 567 Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures 568 569 :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list. 570 :return: tuple with iType name and list of available instruments of current type for defined user token. 571 """ 572 result = [] 573 574 if iType in TKS_INSTRUMENTS: 575 uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType)) 576 577 # all instruments have the same body in API v2 requests: 578 self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"}) # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL] 579 instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType) 580 result = self.SendAPIRequest(instrumentURL, reqType="POST")["instruments"] 581 582 return iType, result 583 584 def _IWrapper(self, kwargs): 585 """ 586 Wrapper runs instrument's update method `_IUpdater()`. 587 It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206 588 """ 589 return self._IUpdater(**kwargs) 590 591 def Listing(self) -> dict: 592 """ 593 Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server. 594 595 :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures. 596 """ 597 uLogger.debug("Requesting all available instruments for current account. Wait, please...") 598 uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES)) 599 600 # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService 601 # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list. 602 iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS] 603 604 poolUpdater = ThreadPool(processes=CPU_USAGES) # create pool for update instruments in parallel mode 605 listing = poolUpdater.map(self._IWrapper, iParams) # execute update operations 606 poolUpdater.close() # close the thread pool 607 poolUpdater.join() # wait a moment until all data returns from threads 608 609 # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures. 610 # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method 611 iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing} 612 613 # calculate minimum price increment (step) for all instruments and set up instrument's type: 614 for iType in iList.keys(): 615 for ticker in iList[iType]: 616 iList[iType][ticker]["type"] = iType 617 618 if "minPriceIncrement" in iList[iType][ticker].keys(): 619 iList[iType][ticker]["step"] = NanoToFloat( 620 iList[iType][ticker]["minPriceIncrement"]["units"], 621 iList[iType][ticker]["minPriceIncrement"]["nano"], 622 ) 623 624 else: 625 iList[iType][ticker]["step"] = 0 # hack to avoid empty value in some instruments, e.g. futures 626 627 return iList 628 629 def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None: 630 """ 631 Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics. 632 633 See also: `DumpInstruments()`, `Listing()`. 634 635 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 636 otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) . 637 """ 638 if self.iListDumpFile is None or not self.iListDumpFile: 639 uLogger.error("Output name of dump file must be defined!") 640 raise Exception("Filename required") 641 642 if not self.iList or forceUpdate: 643 self.iList = self.Listing() 644 645 xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx" 646 647 # Save as XLSX with separated sheets for every type of instruments: 648 with pd.ExcelWriter( 649 path=xlsxDumpFile, 650 date_format=TKS_DATE_FORMAT, 651 datetime_format=TKS_DATE_TIME_FORMAT, 652 mode="w", 653 ) as writer: 654 for iType in TKS_INSTRUMENTS: 655 df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index") # generate pandas object from self.iList dictionary 656 df = df[sorted(df)] # sorted by column names 657 df = df.applymap( 658 lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item, 659 na_action="ignore", 660 ) # converting numbers from nano-type to float in every cell 661 df.to_excel( 662 writer, 663 sheet_name=iType, 664 encoding="UTF-8", 665 freeze_panes=(1, 1), 666 ) # saving as XLSX-file with freeze first row and column as headers 667 668 uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile))) 669 670 def DumpInstruments(self, forceUpdate: bool = True) -> str: 671 """ 672 Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server 673 using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file. 674 675 See also: `DumpInstrumentsAsXLSX()`, `Listing()`. 676 677 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 678 otherwise just saves exist `iList` as JSON-file (default: `dump.json`). 679 :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file. 680 """ 681 if self.iListDumpFile is None or not self.iListDumpFile: 682 uLogger.error("Output name of dump file must be defined!") 683 raise Exception("Filename required") 684 685 if not self.iList or forceUpdate: 686 self.iList = self.Listing() 687 688 jsonDump = json.dumps(self.iList, indent=4, sort_keys=False) # create JSON object as string 689 with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH: 690 fH.write(jsonDump) 691 692 uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile))) 693 694 return jsonDump 695 696 def ShowInstrumentInfo(self, iJSON: dict, show: bool = True, onlyFiles=False) -> str: 697 """ 698 Show information about one instrument defined by json data and prints it in Markdown format. 699 700 See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`. 701 702 :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self._ticker]` 703 :param show: if `True` then also printing information about instrument and its current price. 704 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 705 :return: multilines text in Markdown format with information about one instrument. 706 """ 707 splitLine = "| | |\n" 708 infoText = "" 709 710 if iJSON is not None and iJSON and isinstance(iJSON, dict): 711 info = [ 712 "# Main information\n\n", 713 "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 714 "| Parameters | Values |\n", 715 "|-------------------------------------------------------------|--------------------------------------------------------|\n", 716 "| Ticker: | {:<54} |\n".format(iJSON["ticker"]), 717 "| Full name: | {:<54} |\n".format(iJSON["name"]), 718 ] 719 720 if "sector" in iJSON.keys() and iJSON["sector"]: 721 info.append("| Sector: | {:<54} |\n".format(iJSON["sector"])) 722 723 if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]: 724 info.append("| Country of instrument: | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"]))) 725 726 info.extend([ 727 splitLine, 728 "| FIGI (Financial Instrument Global Identifier): | {:<54} |\n".format(iJSON["figi"]), 729 "| Real exchange [Exchange section]: | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])), 730 ]) 731 732 if "isin" in iJSON.keys() and iJSON["isin"]: 733 info.append("| ISIN (International Securities Identification Number): | {:<54} |\n".format(iJSON["isin"])) 734 735 if "classCode" in iJSON.keys(): 736 info.append("| Class Code (exchange section where instrument is traded): | {:<54} |\n".format(iJSON["classCode"])) 737 738 info.extend([ 739 splitLine, 740 "| Current broker security trading status: | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]), 741 splitLine, 742 "| Buy operations allowed: | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"), 743 "| Sale operations allowed: | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"), 744 "| Short positions allowed: | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"), 745 ]) 746 747 if iJSON["figi"]: 748 self._figi = iJSON["figi"] 749 iJSON = iJSON | self.RequestTradingStatus() 750 751 info.extend([ 752 splitLine, 753 "| Limit orders allowed: | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"), 754 "| Market orders allowed: | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"), 755 "| API trade allowed: | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"), 756 ]) 757 758 info.append(splitLine) 759 760 if "type" in iJSON.keys() and iJSON["type"]: 761 info.append("| Type of the instrument: | {:<54} |\n".format(iJSON["type"])) 762 763 if "shareType" in iJSON.keys() and iJSON["shareType"]: 764 info.append("| Share type: | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]])) 765 766 if "futuresType" in iJSON.keys() and iJSON["futuresType"]: 767 info.append("| Futures type: | {:<54} |\n".format(iJSON["futuresType"])) 768 769 if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]: 770 info.append("| IPO date: | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", ""))) 771 772 if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]: 773 info.append("| Released date: | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", ""))) 774 775 if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]: 776 info.append("| Rebalancing frequency: | {:<54} |\n".format(iJSON["rebalancingFreq"])) 777 778 if "focusType" in iJSON.keys() and iJSON["focusType"]: 779 info.append("| Focusing type: | {:<54} |\n".format(iJSON["focusType"])) 780 781 if "assetType" in iJSON.keys() and iJSON["assetType"]: 782 info.append("| Asset type: | {:<54} |\n".format(iJSON["assetType"])) 783 784 if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]: 785 info.append("| Basic asset: | {:<54} |\n".format(iJSON["basicAsset"])) 786 787 if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]: 788 info.append("| Basic asset size: | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"])))) 789 790 if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]: 791 info.append("| ISO currency name: | {:<54} |\n".format(iJSON["isoCurrencyName"])) 792 793 if "currency" in iJSON.keys(): 794 info.append("| Payment currency: | {:<54} |\n".format(iJSON["currency"])) 795 796 if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys(): 797 info.append("| Nominal currency: | {:<54} |\n".format(iJSON["nominal"]["currency"])) 798 799 if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]: 800 info.append("| First trade date: | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", ""))) 801 802 if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]: 803 info.append("| Last trade date: | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", ""))) 804 805 if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]: 806 info.append("| Date of expiration: | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", ""))) 807 808 if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]: 809 info.append("| State registration date: | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", ""))) 810 811 if "placementDate" in iJSON.keys() and iJSON["placementDate"]: 812 info.append("| Placement date: | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", ""))) 813 814 if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]: 815 info.append("| Maturity date: | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", ""))) 816 817 if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]: 818 info.append("| Perpetual bond: | Yes |\n") 819 820 if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]: 821 info.append("| Over-the-counter (OTC) securities: | Yes |\n") 822 823 iExt = None 824 if iJSON["type"] == "Bonds": 825 info.extend([ 826 splitLine, 827 "| Bond issue (size / plan): | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])), 828 "| Nominal price (100%): | {:<54} |\n".format("{} {}".format( 829 "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."), 830 iJSON["nominal"]["currency"], 831 )), 832 ]) 833 834 if "floatingCouponFlag" in iJSON.keys(): 835 info.append("| Floating coupon: | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No")) 836 837 if "amortizationFlag" in iJSON.keys(): 838 info.append("| Amortization: | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No")) 839 840 info.append(splitLine) 841 842 if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]: 843 info.append("| Number of coupon payments per year: | {:<54} |\n".format(iJSON["couponQuantityPerYear"])) 844 845 if iJSON["figi"]: 846 iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False) # extended bonds data 847 848 info.extend([ 849 "| Days last to maturity date: | {:<54} |\n".format(iExt["daysToMaturity"][0]), 850 "| Coupons yield (average coupon daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])), 851 "| Current price yield (average daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])), 852 ]) 853 854 if "aciValue" in iJSON.keys() and iJSON["aciValue"]: 855 info.append("| Current accumulated coupon income (ACI): | {:<54} |\n".format("{:.2f} {}".format( 856 NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]), 857 iJSON["aciValue"]["currency"] 858 ))) 859 860 if "currentPrice" in iJSON.keys(): 861 info.append(splitLine) 862 863 currency = iJSON["currency"] if "currency" in iJSON.keys() else "" # nominal currency for bonds, otherwise currency of instrument 864 aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else "" # payment currency 865 866 bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0 # previous close price of bond 867 bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0 # last price of bond 868 bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0 # max price of bond 869 bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0 # min price of bond 870 bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0 # delta between last deal price and last close 871 872 curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0 873 curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0 874 875 info.extend([ 876 "| Previous close price of the instrument: | {:<54} |\n".format("{}{}".format( 877 "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A", 878 "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 879 )), 880 "| Last deal price of the instrument: | {:<54} |\n".format("{}{}".format( 881 "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A", 882 "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 883 )), 884 "| Changes between last deal price and last close | {:<54} |\n".format( 885 "{:.2f}%{}".format( 886 iJSON["currentPrice"]["changes"], 887 " ({}{:.2f} {})".format( 888 "+" if bondChangesDelta > 0 else "", 889 bondChangesDelta, 890 aciCurrency 891 ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format( 892 "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "", 893 iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"], 894 currency 895 ), 896 ) 897 ), 898 "| Current limit price, min / max: | {:<54} |\n".format("{}{} / {}{}{}".format( 899 "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A", 900 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 901 "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A", 902 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 903 " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else "" 904 )), 905 "| Actual price, sell / buy: | {:<54} |\n".format("{}{} / {}{}{}".format( 906 "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A", 907 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 908 "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A", 909 "%" if iJSON["type"] == "Bonds" else" {}".format(currency), 910 " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else "" 911 )), 912 ]) 913 914 if "lot" in iJSON.keys(): 915 info.append("| Minimum lot to buy: | {:<54} |\n".format(iJSON["lot"])) 916 917 if "step" in iJSON.keys() and iJSON["step"] != 0: 918 info.append("| Minimum price increment (step): | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else ""))) 919 920 # Add bond payment calendar: 921 if iJSON["type"] == "Bonds": 922 strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False) # bond payment calendar 923 info.extend(["\n#", strCalendar]) 924 925 infoText += "".join(info) 926 927 if show and not onlyFiles: 928 uLogger.info("{}".format(infoText)) 929 930 if self.infoFile is not None and (show or onlyFiles): 931 with open(self.infoFile, "w", encoding="UTF-8") as fH: 932 fH.write(infoText) 933 934 uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile))) 935 936 if self.useHTMLReports: 937 htmlFilePath = self.infoFile.replace(".md", ".html") if self.infoFile.endswith(".md") else self.infoFile + ".html" 938 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 939 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Main information", commonCSS=COMMON_CSS, markdown=infoText)) 940 941 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 942 943 return infoText 944 945 def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict: 946 """ 947 Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined! 948 949 :param requestPrice: if `False` then do not request current price of instrument (because this is long operation). 950 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 951 :return: JSON formatted data with information about instrument. 952 """ 953 tickerJSON = {} 954 if self.moreDebug: 955 uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self._ticker)) 956 957 if not self._ticker: 958 uLogger.warning("self._ticker variable is not be empty!") 959 960 else: 961 if self._ticker in TKS_TICKERS_OR_FIGI_EXCLUDED: 962 uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self._ticker)) 963 raise Exception("Instrument not allowed") 964 965 if not self.iList: 966 self.iList = self.Listing() 967 968 if self._ticker in self.iList["Shares"].keys(): 969 tickerJSON = self.iList["Shares"][self._ticker] 970 if self.moreDebug: 971 uLogger.debug("Ticker [{}] found in shares list".format(self._ticker)) 972 973 elif self._ticker in self.iList["Currencies"].keys(): 974 tickerJSON = self.iList["Currencies"][self._ticker] 975 if self.moreDebug: 976 uLogger.debug("Ticker [{}] found in currencies list".format(self._ticker)) 977 978 elif self._ticker in self.iList["Bonds"].keys(): 979 tickerJSON = self.iList["Bonds"][self._ticker] 980 if self.moreDebug: 981 uLogger.debug("Ticker [{}] found in bonds list".format(self._ticker)) 982 983 elif self._ticker in self.iList["Etfs"].keys(): 984 tickerJSON = self.iList["Etfs"][self._ticker] 985 if self.moreDebug: 986 uLogger.debug("Ticker [{}] found in etfs list".format(self._ticker)) 987 988 elif self._ticker in self.iList["Futures"].keys(): 989 tickerJSON = self.iList["Futures"][self._ticker] 990 if self.moreDebug: 991 uLogger.debug("Ticker [{}] found in futures list".format(self._ticker)) 992 993 if tickerJSON: 994 self._figi = tickerJSON["figi"] 995 996 if requestPrice: 997 tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False) 998 999 if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None: 1000 tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"] 1001 1002 else: 1003 tickerJSON["currentPrice"]["changes"] = 0 1004 1005 if show: 1006 self.ShowInstrumentInfo(iJSON=tickerJSON, show=True) # print info as Markdown text 1007 1008 else: 1009 if show: 1010 uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self._ticker)) 1011 1012 return tickerJSON 1013 1014 def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict: 1015 """ 1016 Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined! 1017 1018 :param requestPrice: if `False` then do not request current price of instrument (it's long operation). 1019 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 1020 :return: JSON formatted data with information about instrument. 1021 """ 1022 figiJSON = {} 1023 if self.moreDebug: 1024 uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self._figi)) 1025 1026 if not self._figi: 1027 uLogger.warning("self._figi variable is not be empty!") 1028 1029 else: 1030 if self._figi in TKS_TICKERS_OR_FIGI_EXCLUDED: 1031 uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self._figi)) 1032 raise Exception("Instrument not allowed") 1033 1034 if not self.iList: 1035 self.iList = self.Listing() 1036 1037 for item in self.iList["Shares"].keys(): 1038 if self._figi == self.iList["Shares"][item]["figi"]: 1039 figiJSON = self.iList["Shares"][item] 1040 1041 if self.moreDebug: 1042 uLogger.debug("FIGI [{}] found in shares list".format(self._figi)) 1043 1044 break 1045 1046 if not figiJSON: 1047 for item in self.iList["Currencies"].keys(): 1048 if self._figi == self.iList["Currencies"][item]["figi"]: 1049 figiJSON = self.iList["Currencies"][item] 1050 1051 if self.moreDebug: 1052 uLogger.debug("FIGI [{}] found in currencies list".format(self._figi)) 1053 1054 break 1055 1056 if not figiJSON: 1057 for item in self.iList["Bonds"].keys(): 1058 if self._figi == self.iList["Bonds"][item]["figi"]: 1059 figiJSON = self.iList["Bonds"][item] 1060 1061 if self.moreDebug: 1062 uLogger.debug("FIGI [{}] found in bonds list".format(self._figi)) 1063 1064 break 1065 1066 if not figiJSON: 1067 for item in self.iList["Etfs"].keys(): 1068 if self._figi == self.iList["Etfs"][item]["figi"]: 1069 figiJSON = self.iList["Etfs"][item] 1070 1071 if self.moreDebug: 1072 uLogger.debug("FIGI [{}] found in etfs list".format(self._figi)) 1073 1074 break 1075 1076 if not figiJSON: 1077 for item in self.iList["Futures"].keys(): 1078 if self._figi == self.iList["Futures"][item]["figi"]: 1079 figiJSON = self.iList["Futures"][item] 1080 1081 if self.moreDebug: 1082 uLogger.debug("FIGI [{}] found in futures list".format(self._figi)) 1083 1084 break 1085 1086 if figiJSON: 1087 self._figi = figiJSON["figi"] 1088 self._ticker = figiJSON["ticker"] 1089 1090 if requestPrice: 1091 figiJSON["currentPrice"] = self.GetCurrentPrices(show=False) 1092 1093 if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None: 1094 figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"] 1095 1096 else: 1097 figiJSON["currentPrice"]["changes"] = 0 1098 1099 if show: 1100 self.ShowInstrumentInfo(iJSON=figiJSON, show=True) # print info as Markdown text 1101 1102 else: 1103 if show: 1104 uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self._figi)) 1105 1106 return figiJSON 1107 1108 def GetCurrentPrices(self, show: bool = True) -> dict: 1109 """ 1110 Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5: 1111 `{"buy": [{"price": 1243.8, "quantity": 193}, 1112 {"price": 1244.0, "quantity": 168}, 1113 {"price": 1244.8, "quantity": 5}, 1114 {"price": 1245.0, "quantity": 61}, 1115 {"price": 1245.4, "quantity": 60}], 1116 "sell": [{"price": 1243.6, "quantity": 8}, 1117 {"price": 1242.6, "quantity": 10}, 1118 {"price": 1242.4, "quantity": 18}, 1119 {"price": 1242.2, "quantity": 50}, 1120 {"price": 1242.0, "quantity": 113}], 1121 "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean: 1122 - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order 1123 - sell: list of dicts with Buyers prices, 1124 - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument), 1125 - quantity: volume value by current price in lots, 1126 - limitUp: current trade session limit price, maximum, 1127 - limitDown: current trade session limit price, minimum, 1128 - lastPrice: last deal price of the instrument, 1129 - closePrice: previous trade session close price of the instrument. 1130 1131 See also: `SearchByTicker()` and `SearchByFIGI()`. 1132 REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1133 Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1134 1135 :param show: if `True` then print DOM to log and console. 1136 :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`. 1137 If an error occurred then returns an empty record: 1138 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`. 1139 """ 1140 prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0} 1141 1142 if self.depth < 1: 1143 uLogger.error("Depth of Market (DOM) must be >=1!") 1144 raise Exception("Incorrect value") 1145 1146 if not (self._ticker or self._figi): 1147 uLogger.error("self._ticker or self._figi variables must be defined!") 1148 raise Exception("Ticker or FIGI required") 1149 1150 if self._ticker and not self._figi: 1151 instrumentByTicker = self.SearchByTicker(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1152 self._figi = instrumentByTicker["figi"] if instrumentByTicker else "" 1153 1154 if not self._ticker and self._figi: 1155 instrumentByFigi = self.SearchByFIGI(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1156 self._ticker = instrumentByFigi["ticker"] if instrumentByFigi else "" 1157 1158 if not self._figi: 1159 uLogger.error("FIGI is not defined!") 1160 raise Exception("Ticker or FIGI required") 1161 1162 else: 1163 uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self._ticker, self._figi)) 1164 1165 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1166 priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook" 1167 self.body = str({"figi": self._figi, "depth": self.depth}) 1168 pricesResponse = self.SendAPIRequest(priceURL, reqType="POST") # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1169 1170 if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()): 1171 # list of dicts with sellers orders: 1172 prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]] 1173 1174 # list of dicts with buyers orders: 1175 prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]] 1176 1177 # max price of instrument at this time: 1178 prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None 1179 1180 # min price of instrument at this time: 1181 prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None 1182 1183 # last price of deal with instrument: 1184 prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0 1185 1186 # last close price of instrument: 1187 prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0 1188 1189 else: 1190 uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi)) 1191 uLogger.debug("Server response: {}".format(pricesResponse)) 1192 1193 if show: 1194 if prices["buy"] or prices["sell"]: 1195 info = [ 1196 "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format( 1197 datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 1198 self._ticker, 1199 self._figi, 1200 self.depth, 1201 ), 1202 "-" * 60, "\n", 1203 " Orders of Buyers | Orders of Sellers\n", 1204 "-" * 60, "\n", 1205 " Sell prices (volumes) | Buy prices (volumes)\n", 1206 "-" * 60, "\n", 1207 ] 1208 1209 if not prices["buy"]: 1210 info.append(" | No orders!\n") 1211 sumBuy = 0 1212 1213 else: 1214 sumBuy = sum([x["quantity"] for x in prices["buy"]]) 1215 maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True) 1216 for item in maxMinSorted: 1217 info.append(" | {} ({})\n".format(item["price"], item["quantity"])) 1218 1219 if not prices["sell"]: 1220 info.append("No orders! |\n") 1221 sumSell = 0 1222 1223 else: 1224 sumSell = sum([x["quantity"] for x in prices["sell"]]) 1225 for item in prices["sell"]: 1226 info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"]))) 1227 1228 info.extend([ 1229 "-" * 60, "\n", 1230 "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)), 1231 "-" * 60, "\n", 1232 ]) 1233 1234 infoText = "".join(info) 1235 1236 uLogger.info("Current prices in order book:\n\n{}".format(infoText)) 1237 1238 else: 1239 uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi)) 1240 1241 return prices 1242 1243 def ShowInstrumentsInfo(self, show: bool = True, onlyFiles=False) -> str: 1244 """ 1245 This method get and show information about all available broker instruments for current user account. 1246 If `instrumentsFile` string is not empty then also save information to this file. 1247 1248 :param show: if `True` then print results to console, if `False` — print only to file. 1249 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 1250 :return: multi-lines string with all available broker instruments. 1251 """ 1252 if not self.iList: 1253 self.iList = self.Listing() 1254 1255 info = [ 1256 "# All available instruments from Tinkoff Broker server for current user token\n\n", 1257 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 1258 ] 1259 1260 # add instruments count by type: 1261 for iType in self.iList.keys(): 1262 info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType]))) 1263 1264 headerLine = "| Ticker | Full name | FIGI | Cur | Lot | Step |\n" 1265 splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n" 1266 1267 # generating info tables with all instruments by type: 1268 for iType in self.iList.keys(): 1269 info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine]) 1270 1271 for instrument in self.iList[iType].keys(): 1272 iName = self.iList[iType][instrument]["name"] # instrument's name 1273 if len(iName) > 57: 1274 iName = "{}...".format(iName[:54]) # right trim for a long string 1275 1276 info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format( 1277 self.iList[iType][instrument]["ticker"], 1278 iName, 1279 self.iList[iType][instrument]["figi"], 1280 self.iList[iType][instrument]["currency"], 1281 self.iList[iType][instrument]["lot"], 1282 "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0, 1283 )) 1284 1285 infoText = "".join(info) 1286 1287 if show and not onlyFiles: 1288 uLogger.info(infoText) 1289 1290 if self.instrumentsFile and (show or onlyFiles): 1291 with open(self.instrumentsFile, "w", encoding="UTF-8") as fH: 1292 fH.write(infoText) 1293 1294 uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile))) 1295 1296 if self.useHTMLReports: 1297 htmlFilePath = self.instrumentsFile.replace(".md", ".html") if self.instrumentsFile.endswith(".md") else self.instrumentsFile + ".html" 1298 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1299 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="List of instruments", commonCSS=COMMON_CSS, markdown=infoText)) 1300 1301 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1302 1303 return infoText 1304 1305 def SearchInstruments(self, pattern: str, show: bool = True, onlyFiles=False) -> dict: 1306 """ 1307 This method search and show information about instruments by part of its ticker, FIGI or name. 1308 If `searchResultsFile` string is not empty then also save information to this file. 1309 1310 :param pattern: string with part of ticker, FIGI or instrument's name. 1311 :param show: if `True` then print results to console, if `False` — return list of result only. 1312 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 1313 :return: list of dictionaries with all found instruments. 1314 """ 1315 if not self.iList: 1316 self.iList = self.Listing() 1317 1318 searchResults = {iType: {} for iType in self.iList} # same as iList but will contain only filtered instruments 1319 compiledPattern = re.compile(pattern, re.IGNORECASE) 1320 1321 for iType in self.iList: 1322 for instrument in self.iList[iType].values(): 1323 searchResult = compiledPattern.search(" ".join( 1324 [instrument["ticker"], instrument["figi"], instrument["name"]] 1325 )) 1326 1327 if searchResult: 1328 searchResults[iType][instrument["ticker"]] = instrument 1329 1330 resultsLen = sum([len(searchResults[iType]) for iType in searchResults]) 1331 info = [ 1332 "# Search results\n\n", 1333 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 1334 "* **Search pattern:** [{}]\n".format(pattern), 1335 "* **Found instruments:** [{}]\n\n".format(resultsLen), 1336 '**Note:** you can view info about found instruments with key "--info", e.g.: "tksbrokerapi -t TICKER --info" or "tksbrokerapi -f FIGI --info".\n' 1337 ] 1338 infoShort = info[:] 1339 1340 headerLine = "| Type | Ticker | Full name | FIGI |\n" 1341 splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n" 1342 skippedLine = "| ... | ... | ... | ... |\n" 1343 1344 if resultsLen == 0: 1345 info.append("\nNo results\n") 1346 infoShort.append("\nNo results\n") 1347 uLogger.warning("No results. Try changing your search pattern.") 1348 1349 else: 1350 for iType in searchResults: 1351 iTypeValuesCount = len(searchResults[iType].values()) 1352 if iTypeValuesCount > 0: 1353 info.extend(["\n## {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1354 infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1355 1356 for instrument in searchResults[iType].values(): 1357 info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format( 1358 instrument["type"], 1359 instrument["ticker"], 1360 "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"], # right trim for a long string 1361 instrument["figi"], 1362 )) 1363 1364 if iTypeValuesCount <= 5: 1365 infoShort.extend(info[-iTypeValuesCount:]) 1366 1367 else: 1368 infoShort.extend(info[-5:]) 1369 infoShort.append(skippedLine) 1370 1371 infoText = "".join(info) 1372 infoTextShort = "".join(infoShort) 1373 1374 if show and not onlyFiles: 1375 uLogger.info(infoTextShort) 1376 uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`") 1377 1378 if self.searchResultsFile and (show or onlyFiles): 1379 with open(self.searchResultsFile, "w", encoding="UTF-8") as fH: 1380 fH.write(infoText) 1381 1382 uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile))) 1383 1384 if self.useHTMLReports: 1385 htmlFilePath = self.searchResultsFile.replace(".md", ".html") if self.searchResultsFile.endswith(".md") else self.searchResultsFile + ".html" 1386 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1387 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Search results", commonCSS=COMMON_CSS, markdown=infoText)) 1388 1389 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1390 1391 return searchResults 1392 1393 def GetUniqueFIGIs(self, instruments: list[str]) -> list: 1394 """ 1395 Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs. 1396 1397 :param instruments: list of strings with tickers or FIGIs. 1398 :return: list with unique instrument FIGIs only. 1399 """ 1400 requestedInstruments = [] 1401 for iName in instruments: 1402 if iName not in self.aliases.keys(): 1403 if iName not in requestedInstruments: 1404 requestedInstruments.append(iName) 1405 1406 else: 1407 if iName not in requestedInstruments: 1408 if self.aliases[iName] not in requestedInstruments: 1409 requestedInstruments.append(self.aliases[iName]) 1410 1411 uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments)) 1412 1413 onlyUniqueFIGIs = [] 1414 for iName in requestedInstruments: 1415 if iName in TKS_TICKERS_OR_FIGI_EXCLUDED: 1416 continue 1417 1418 self._ticker = iName 1419 iData = self.SearchByTicker(requestPrice=False) # trying to find instrument by ticker 1420 1421 if not iData: 1422 self._ticker = "" 1423 self._figi = iName 1424 1425 iData = self.SearchByFIGI(requestPrice=False) # trying to find instrument by FIGI 1426 1427 if not iData: 1428 self._figi = "" 1429 uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName)) 1430 1431 if iData and iData["figi"] not in onlyUniqueFIGIs: 1432 onlyUniqueFIGIs.append(iData["figi"]) 1433 1434 uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs)) 1435 1436 return onlyUniqueFIGIs 1437 1438 def GetListOfPrices(self, instruments: list[str], show: bool = False, onlyFiles=False) -> list[dict]: 1439 """ 1440 This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation! 1441 1442 See limits: https://tinkoff.github.io/investAPI/limits/ 1443 1444 If `pricesFile` string is not empty then also save information to this file. 1445 1446 :param instruments: list of strings with tickers or FIGIs. 1447 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1448 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 1449 :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1450 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods. 1451 """ 1452 if instruments is None or not instruments: 1453 uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!") 1454 raise Exception("Ticker or FIGI required") 1455 1456 onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments) 1457 1458 uLogger.debug("Requesting current prices from Tinkoff Broker server...") 1459 1460 iList = [] # trying to get info and current prices about all unique instruments: 1461 for self._figi in onlyUniqueFIGIs: 1462 iData = self.SearchByFIGI(requestPrice=True, show=False) 1463 iList.append(iData) 1464 1465 self.ShowListOfPrices(iList, show, onlyFiles) 1466 1467 return iList 1468 1469 def ShowListOfPrices(self, iList: list, show: bool = True, onlyFiles=False) -> str: 1470 """ 1471 Show table contains current prices of given instruments. 1472 1473 :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1474 One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods. 1475 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1476 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 1477 :return: multilines text in Markdown format as a table contains current prices. 1478 """ 1479 infoText = "" 1480 1481 if show or self.pricesFile or onlyFiles: 1482 info = [ 1483 "# Current prices\n\n* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1484 "| Ticker | FIGI | Type | Prev. close | Last price | Chg. % | Day limits min/max | Actual sell / buy | Curr. |\n", 1485 "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n", 1486 ] 1487 1488 for item in iList: 1489 info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format( 1490 item["ticker"], 1491 item["figi"], 1492 item["type"], 1493 "{:.2f}".format(float(item["currentPrice"]["closePrice"])), 1494 "{:.2f}".format(float(item["currentPrice"]["lastPrice"])), 1495 "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])), 1496 "{} / {}".format( 1497 item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A", 1498 item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A", 1499 ), 1500 "{} / {}".format( 1501 item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A", 1502 item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A", 1503 ), 1504 item["currency"], 1505 )) 1506 1507 infoText = "".join(info) 1508 1509 if show and not onlyFiles: 1510 uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText)) 1511 1512 if self.pricesFile and (show or onlyFiles): 1513 with open(self.pricesFile, "w", encoding="UTF-8") as fH: 1514 fH.write(infoText) 1515 1516 uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile))) 1517 1518 if self.useHTMLReports: 1519 htmlFilePath = self.pricesFile.replace(".md", ".html") if self.pricesFile.endswith(".md") else self.pricesFile + ".html" 1520 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1521 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Current prices", commonCSS=COMMON_CSS, markdown=infoText)) 1522 1523 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1524 1525 return infoText 1526 1527 def RequestTradingStatus(self) -> dict: 1528 """ 1529 Requesting trading status for the instrument defined by `figi` variable. 1530 1531 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus 1532 1533 Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest 1534 1535 :return: dictionary with trading status attributes. Response example: 1536 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", 1537 "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}` 1538 """ 1539 if self._figi is None or not self._figi: 1540 uLogger.error("Variable `figi` must be defined for using this method!") 1541 raise Exception("FIGI required") 1542 1543 uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self._figi)) 1544 1545 self.body = str({"figi": self._figi, "instrumentId": self._figi}) 1546 tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus" 1547 tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST") 1548 1549 if self.moreDebug: 1550 uLogger.debug("Records about current trading status successfully received") 1551 1552 return tradingStatus 1553 1554 def RequestPortfolio(self) -> dict: 1555 """ 1556 Requesting actual user's portfolio for current `accountId`. 1557 1558 REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio 1559 1560 Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest 1561 1562 :return: dictionary with user's portfolio. 1563 """ 1564 if self.accountId is None or not self.accountId: 1565 uLogger.error("Variable `accountId` must be defined for using this method!") 1566 raise Exception("Account ID required") 1567 1568 uLogger.debug("Requesting current actual user's portfolio. Wait, please...") 1569 1570 self.body = str({"accountId": self.accountId}) 1571 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio" 1572 rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST") 1573 1574 if self.moreDebug: 1575 uLogger.debug("Records about user's portfolio successfully received") 1576 1577 return rawPortfolio 1578 1579 def RequestPositions(self) -> dict: 1580 """ 1581 Requesting open positions by currencies and instruments for current `accountId`. 1582 1583 REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions 1584 1585 Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest 1586 1587 :return: dictionary with open positions by instruments. 1588 """ 1589 if self.accountId is None or not self.accountId: 1590 uLogger.error("Variable `accountId` must be defined for using this method!") 1591 raise Exception("Account ID required") 1592 1593 uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...") 1594 1595 self.body = str({"accountId": self.accountId}) 1596 positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions" 1597 rawPositions = self.SendAPIRequest(positionsURL, reqType="POST") 1598 1599 if self.moreDebug: 1600 uLogger.debug("Records about current open positions successfully received") 1601 1602 return rawPositions 1603 1604 def RequestPendingOrders(self) -> list: 1605 """ 1606 Requesting current actual pending limit orders for current `accountId`. 1607 1608 REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders 1609 1610 Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest 1611 1612 :return: list of dictionaries with pending limit orders. 1613 """ 1614 if self.accountId is None or not self.accountId: 1615 uLogger.error("Variable `accountId` must be defined for using this method!") 1616 raise Exception("Account ID required") 1617 1618 uLogger.debug("Requesting current actual pending limit orders. Wait, please...") 1619 1620 self.body = str({"accountId": self.accountId}) 1621 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders" 1622 rawResponse = self.SendAPIRequest(ordersURL, reqType="POST") 1623 1624 if "orders" in rawResponse.keys(): 1625 rawOrders = rawResponse["orders"] 1626 uLogger.debug("[{}] records about pending limit orders received".format(len(rawOrders))) 1627 1628 else: 1629 rawOrders = [] 1630 uLogger.debug("No pending limit orders returned! rawResponse = {}".format(rawResponse)) 1631 1632 return rawOrders 1633 1634 def RequestStopOrders(self) -> list: 1635 """ 1636 Requesting current actual stop orders for current `accountId`. 1637 1638 REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders 1639 1640 Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest 1641 1642 :return: list of dictionaries with stop orders. 1643 """ 1644 if self.accountId is None or not self.accountId: 1645 uLogger.error("Variable `accountId` must be defined for using this method!") 1646 raise Exception("Account ID required") 1647 1648 uLogger.debug("Requesting current actual stop orders. Wait, please...") 1649 1650 self.body = str({"accountId": self.accountId}) 1651 stopOrdersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders" 1652 rawResponse = self.SendAPIRequest(stopOrdersURL, reqType="POST") 1653 1654 if "stopOrders" in rawResponse.keys(): 1655 rawStopOrders = rawResponse["stopOrders"] 1656 uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders))) 1657 1658 else: 1659 rawStopOrders = [] 1660 uLogger.debug("No stop orders returned! rawResponse = {}".format(rawResponse)) 1661 1662 return rawStopOrders 1663 1664 def Overview(self, show: bool = False, details: str = "full", onlyFiles=False) -> dict: 1665 """ 1666 Get portfolio: all open positions, orders and some statistics for current `accountId`. 1667 If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile` 1668 and `overviewBondsCalendarFile` are defined then also save information to file. 1669 1670 WARNING! It is not recommended to run this method too many times in a loop! The server receives 1671 many requests about the state of the portfolio, and then, based on the received data, a large number 1672 of calculation and statistics are collected. 1673 1674 :param show: if `False` then only dictionary returns, if `True` then show more debug information. 1675 :param details: how detailed should the information be? 1676 - `full` — shows full available information about portfolio status (by default), 1677 - `positions` — shows only open positions, 1678 - `orders` — shows only sections of open limits and stop orders. 1679 - `digest` — show a short digest of the portfolio status, 1680 - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories, 1681 - `calendar` — shows only the bonds calendar section (if these present in portfolio). 1682 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 1683 :return: dictionary with client's raw portfolio and some statistics. 1684 """ 1685 if self.accountId is None or not self.accountId: 1686 uLogger.error("Variable `accountId` must be defined for using this method!") 1687 raise Exception("Account ID required") 1688 1689 view = { 1690 "raw": { # --- raw portfolio responses from broker with user portfolio data: 1691 "headers": {}, # list of dictionaries, response headers without "positions" section 1692 "Currencies": [], # list of dictionaries, open trades with currencies from "positions" section 1693 "Shares": [], # list of dictionaries, open trades with shares from "positions" section 1694 "Bonds": [], # list of dictionaries, open trades with bonds from "positions" section 1695 "Etfs": [], # list of dictionaries, open trades with etfs from "positions" section 1696 "Futures": [], # list of dictionaries, open trades with futures from "positions" section 1697 "positions": {}, # raw response from broker: dictionary with current available or blocked currencies and instruments for client 1698 "orders": [], # raw response from broker: list of dictionaries with all pending (market) orders 1699 "stopOrders": [], # raw response from broker: list of dictionaries with all stop orders 1700 "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}}, # dict with prices of all currencies in RUB 1701 }, 1702 "stat": { # --- some statistics calculated using "raw" sections: 1703 "portfolioCostRUB": 0., # portfolio cost in RUB (Russian Rouble) 1704 "availableRUB": 0., # available rubles (without other currencies) 1705 "blockedRUB": 0., # blocked sum in Russian Rouble 1706 "totalChangesRUB": 0., # changes for all open trades in RUB 1707 "totalChangesPercentRUB": 0., # changes for all open trades in percents 1708 "allCurrenciesCostRUB": 0., # costs of all currencies (include rubles) in RUB 1709 "sharesCostRUB": 0., # costs of all shares in RUB 1710 "bondsCostRUB": 0., # costs of all bonds in RUB 1711 "etfsCostRUB": 0., # costs of all etfs in RUB 1712 "futuresCostRUB": 0., # costs of all futures in RUB 1713 "Currencies": [], # list of dictionaries of all currencies statistics 1714 "Shares": [], # list of dictionaries of all shares statistics 1715 "Bonds": [], # list of dictionaries of all bonds statistics 1716 "Etfs": [], # list of dictionaries of all etfs statistics 1717 "Futures": [], # list of dictionaries of all futures statistics 1718 "orders": [], # list of dictionaries of all pending (market) orders and it's parameters 1719 "stopOrders": [], # list of dictionaries of all stop orders and it's parameters 1720 "blockedCurrencies": {}, # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21} 1721 "blockedInstruments": {}, # dict with blocked by FIGI, e.g. {} 1722 "funds": {}, # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1723 }, 1724 "analytics": { # --- some analytics of portfolio: 1725 "distrByAssets": {}, # portfolio distribution by assets 1726 "distrByCompanies": {}, # portfolio distribution by companies 1727 "distrBySectors": {}, # portfolio distribution by sectors 1728 "distrByCurrencies": {}, # portfolio distribution by currencies 1729 "distrByCountries": {}, # portfolio distribution by countries 1730 "bondsCalendar": None, # bonds payment calendar as Pandas DataFrame (if these present in portfolio) 1731 } 1732 } 1733 1734 details = details.lower() 1735 availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"] 1736 if details not in availableDetails: 1737 details = "full" 1738 uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails)) 1739 1740 uLogger.debug("Requesting portfolio of a client. Wait, please...") 1741 1742 portfolioResponse = self.RequestPortfolio() # current user's portfolio (dict) 1743 view["raw"]["positions"] = self.RequestPositions() # current open positions by instruments (dict) 1744 view["raw"]["orders"] = self.RequestPendingOrders() # current actual pending limit orders (list) 1745 view["raw"]["stopOrders"] = self.RequestStopOrders() # current actual stop orders (list) 1746 1747 # save response headers without "positions" section: 1748 for key in portfolioResponse.keys(): 1749 if key != "positions": 1750 view["raw"]["headers"][key] = portfolioResponse[key] 1751 1752 else: 1753 continue 1754 1755 # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation 1756 # Type of instrument must be only one of supported types in TKS_INSTRUMENTS 1757 for item in portfolioResponse["positions"]: 1758 if item["instrumentType"] == "currency": 1759 self._figi = item["figi"] 1760 if not self._figi and item["ticker"]: 1761 self._ticker = item["ticker"] 1762 self._figi = self.SearchByTicker()["figi"] # Get FIGI to avoid warnings 1763 1764 curr = self.SearchByFIGI(requestPrice=False) 1765 1766 # current price of currency in RUB: 1767 view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = { 1768 "name": curr["name"], 1769 "currentPrice": NanoToFloat( 1770 item["currentPrice"]["units"], 1771 item["currentPrice"]["nano"] 1772 ), 1773 } 1774 1775 view["raw"]["Currencies"].append(item) 1776 1777 elif item["instrumentType"] == "share": 1778 view["raw"]["Shares"].append(item) 1779 1780 elif item["instrumentType"] == "bond": 1781 view["raw"]["Bonds"].append(item) 1782 1783 elif item["instrumentType"] == "etf": 1784 view["raw"]["Etfs"].append(item) 1785 1786 elif item["instrumentType"] == "futures": 1787 view["raw"]["Futures"].append(item) 1788 1789 else: 1790 continue 1791 1792 # how many volume of currencies (by ISO currency name) are blocked: 1793 for item in view["raw"]["positions"]["blocked"]: 1794 blocked = NanoToFloat(item["units"], item["nano"]) 1795 if blocked > 0: 1796 view["stat"]["blockedCurrencies"][item["currency"]] = blocked 1797 1798 # how many volume of instruments (by FIGI) are blocked: 1799 for item in view["raw"]["positions"]["securities"]: 1800 blocked = int(item["blocked"]) 1801 if blocked > 0: 1802 view["stat"]["blockedInstruments"][item["figi"]] = blocked 1803 1804 allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]} 1805 1806 if "rub" in allBlocked.keys(): 1807 view["stat"]["blockedRUB"] = allBlocked["rub"] # blocked rubles 1808 1809 # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies: 1810 view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"]) 1811 view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"]) 1812 view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"]) 1813 view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"]) 1814 view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"]) 1815 view["stat"]["portfolioCostRUB"] = sum([ 1816 view["stat"]["allCurrenciesCostRUB"], 1817 view["stat"]["sharesCostRUB"], 1818 view["stat"]["bondsCostRUB"], 1819 view["stat"]["etfsCostRUB"], 1820 view["stat"]["futuresCostRUB"], 1821 ]) 1822 1823 # --- calculating some portfolio statistics: 1824 byComp = {} # distribution by companies 1825 bySect = {} # distribution by sectors 1826 byCurr = {} # distribution by currencies (include RUB) 1827 unknownCountryName = "All other countries" # default name for instruments without "countryOfRisk" and "countryOfRiskName" 1828 byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}} # distribution by countries (currencies are included in their countries) 1829 1830 for item in portfolioResponse["positions"]: 1831 self._figi = item["figi"] 1832 if not self._figi and item["ticker"]: 1833 self._ticker = item["ticker"] 1834 self._figi = self.SearchByTicker()["figi"] # Get FIGI to avoid warnings 1835 1836 instrument = self.SearchByFIGI(requestPrice=False) # full raw info about instrument by FIGI 1837 1838 if instrument: 1839 if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys(): 1840 blocked = allBlocked[instrument["nominal"]["currency"]] # blocked volume of currency 1841 1842 elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys(): 1843 blocked = allBlocked[item["figi"]] # blocked volume of other instruments 1844 1845 else: 1846 blocked = 0 1847 1848 volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"]) # available volume of instrument 1849 lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"]) # available volume in lots of instrument 1850 direction = "Long" if lots >= 0 else "Short" # direction of an instrument's position: short or long 1851 curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"]) # current instrument's price 1852 average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"]) # current average position price 1853 profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"]) # expected profit at current moment 1854 currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"] # currency name rub, usd, eur etc. 1855 cost = curPrice if "currentNkd" not in item.keys() else (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume # current cost of all volume of instrument in basic asset 1856 baseCurrencyName = item["currentPrice"]["currency"] # name of base currency (rub) 1857 countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName 1858 costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"] # cost in rubles 1859 percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0. # instrument's part in percent of full portfolio cost 1860 1861 statData = { 1862 "figi": item["figi"], # FIGI from REST API "GetPortfolio" method 1863 "ticker": instrument["ticker"], # ticker by FIGI 1864 "currency": currency, # currency name rub, usd, eur etc. for instrument price 1865 "volume": volume, # available volume of instrument 1866 "lots": lots, # volume in lots of instrument 1867 "direction": direction, # direction of an instrument's position: short or long 1868 "blocked": blocked, # blocked volume of currency or instrument 1869 "currentPrice": curPrice, # current instrument's price in basic asset 1870 "average": average, # current average position price 1871 "cost": cost, # current cost of all volume of instrument in basic asset 1872 "baseCurrencyName": baseCurrencyName, # name of base currency (rub) 1873 "costRUB": costRUB, # cost of instrument in ruble 1874 "percentCostRUB": percentCostRUB, # instrument's part in percent of full portfolio cost in RUB 1875 "profit": profit, # expected profit at current moment 1876 "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0, # expected percents of profit at current moment for this instrument 1877 "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other", 1878 "name": instrument["name"] if "name" in instrument.keys() else "", # human-readable names of instruments 1879 "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "", # ISO name for currencies only 1880 "country": countryName, # e.g. "[RU] Российская Федерация" or unknownCountryName 1881 "step": instrument["step"], # minimum price increment 1882 } 1883 1884 # adding distribution by unique countries: 1885 if statData["country"] not in byCountry.keys(): 1886 byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB} 1887 1888 else: 1889 byCountry[statData["country"]]["cost"] += costRUB 1890 byCountry[statData["country"]]["percent"] += percentCostRUB 1891 1892 if item["instrumentType"] != "currency": 1893 # adding distribution by unique companies: 1894 if statData["name"]: 1895 if statData["name"] not in byComp.keys(): 1896 byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB} 1897 1898 else: 1899 byComp[statData["name"]]["cost"] += costRUB 1900 byComp[statData["name"]]["percent"] += percentCostRUB 1901 1902 # adding distribution by unique sectors: 1903 if statData["sector"] not in bySect.keys(): 1904 bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB} 1905 1906 else: 1907 bySect[statData["sector"]]["cost"] += costRUB 1908 bySect[statData["sector"]]["percent"] += percentCostRUB 1909 1910 # adding distribution by unique currencies: 1911 if currency not in byCurr.keys(): 1912 byCurr[currency] = { 1913 "name": view["raw"]["currenciesCurrentPrices"][currency]["name"], 1914 "cost": costRUB, 1915 "percent": percentCostRUB 1916 } 1917 1918 else: 1919 byCurr[currency]["cost"] += costRUB 1920 byCurr[currency]["percent"] += percentCostRUB 1921 1922 # saving statistics for every instrument: 1923 if item["instrumentType"] == "currency": 1924 view["stat"]["Currencies"].append(statData) 1925 1926 # update dict with free funds for trading (total - blocked) by currencies 1927 # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1928 view["stat"]["funds"][currency] = { 1929 "total": volume, 1930 "totalCostRUB": costRUB, # total volume cost in rubles 1931 "free": volume - blocked, 1932 "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0, # free volume cost in rubles 1933 } 1934 1935 elif item["instrumentType"] == "share": 1936 view["stat"]["Shares"].append(statData) 1937 1938 elif item["instrumentType"] == "bond": 1939 view["stat"]["Bonds"].append(statData) 1940 1941 elif item["instrumentType"] == "etf": 1942 view["stat"]["Etfs"].append(statData) 1943 1944 elif item["instrumentType"] == "Futures": 1945 view["stat"]["Futures"].append(statData) 1946 1947 else: 1948 continue 1949 1950 # total changes in Russian Ruble: 1951 view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]]) # available RUB without other currencies 1952 view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0. 1953 startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100) 1954 view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost 1955 view["stat"]["funds"]["rub"] = { 1956 "total": view["stat"]["availableRUB"], 1957 "totalCostRUB": view["stat"]["availableRUB"], 1958 "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1959 "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1960 } 1961 1962 # --- pending limit orders sector data: 1963 uniquePendingOrdersFIGIs = [] # unique FIGIs of pending limit orders to avoid many times price requests 1964 uniquePendingOrders = {} # unique instruments with FIGIs as dictionary keys 1965 1966 for item in view["raw"]["orders"]: 1967 self._figi = item["figi"] 1968 1969 if item["figi"] not in uniquePendingOrdersFIGIs: 1970 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1971 1972 uniquePendingOrdersFIGIs.append(item["figi"]) 1973 uniquePendingOrders[item["figi"]] = instrument 1974 1975 else: 1976 instrument = uniquePendingOrders[item["figi"]] 1977 1978 if instrument: 1979 action = TKS_ORDER_DIRECTIONS[item["direction"]] 1980 orderType = TKS_ORDER_TYPES[item["orderType"]] 1981 orderState = TKS_ORDER_STATES[item["executionReportStatus"]] 1982 orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1983 1984 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1985 if item["direction"] == "ORDER_DIRECTION_BUY": 1986 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1987 1988 else: 1989 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1990 1991 # requested price for order execution: 1992 target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"]) 1993 1994 # necessary changes in percent to reach target from current price: 1995 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1996 1997 view["stat"]["orders"].append({ 1998 "orderID": item["orderId"], # orderId number parameter of current order 1999 "figi": item["figi"], # FIGI identification 2000 "ticker": instrument["ticker"], # ticker name by FIGI 2001 "lotsRequested": item["lotsRequested"], # requested lots value 2002 "lotsExecuted": item["lotsExecuted"], # how many lots are executed 2003 "currentPrice": lastPrice, # current instrument's price for defined action 2004 "targetPrice": target, # requested price for order execution in base currency 2005 "baseCurrencyName": item["initialSecurityPrice"]["currency"], # name of base currency 2006 "percentChanges": changes, # changes in percent to target from current price 2007 "currency": item["currency"], # instrument's currency name 2008 "action": action, # sell / buy / Unknown from TKS_ORDER_DIRECTIONS 2009 "type": orderType, # type of order from TKS_ORDER_TYPES 2010 "status": orderState, # order status from TKS_ORDER_STATES 2011 "date": orderDate, # string with order date and time from UTC format (without nano seconds part) 2012 }) 2013 2014 # --- stop orders sector data: 2015 uniqueStopOrdersFIGIs = [] # unique FIGIs of stop orders to avoid many times price requests 2016 uniqueStopOrders = {} # unique instruments with FIGIs as dictionary keys 2017 2018 for item in view["raw"]["stopOrders"]: 2019 self._figi = item["figi"] 2020 2021 if item["figi"] not in uniqueStopOrdersFIGIs: 2022 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 2023 2024 uniqueStopOrdersFIGIs.append(item["figi"]) 2025 uniqueStopOrders[item["figi"]] = instrument 2026 2027 else: 2028 instrument = uniqueStopOrders[item["figi"]] 2029 2030 if instrument: 2031 action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]] 2032 orderType = TKS_STOP_ORDER_TYPES[item["orderType"]] 2033 createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 2034 2035 # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order 2036 if "expirationTime" in item.keys(): 2037 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"] 2038 expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0] 2039 2040 else: 2041 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"] 2042 expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"] 2043 2044 # current instrument's price (last sellers order if buy, and last buyers order if sell): 2045 if item["direction"] == "STOP_ORDER_DIRECTION_BUY": 2046 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 2047 2048 else: 2049 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 2050 2051 # requested price when stop-order executed: 2052 target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"]) 2053 2054 # price for limit-order, set up when stop-order executed: 2055 limit = NanoToFloat(item["price"]["units"], item["price"]["nano"]) 2056 2057 # necessary changes in percent to reach target from current price: 2058 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 2059 2060 view["stat"]["stopOrders"].append({ 2061 "orderID": item["stopOrderId"], # stopOrderId number parameter of current stop-order 2062 "figi": item["figi"], # FIGI identification 2063 "ticker": instrument["ticker"], # ticker name by FIGI 2064 "lotsRequested": item["lotsRequested"], # requested lots value 2065 "currentPrice": lastPrice, # current instrument's price for defined action 2066 "targetPrice": target, # requested price for stop-order execution in base currency 2067 "limitPrice": limit, # price for limit-order, set up when stop-order executed, 0 if market order 2068 "baseCurrencyName": item["stopPrice"]["currency"], # name of base currency 2069 "percentChanges": changes, # changes in percent to target from current price 2070 "currency": item["currency"], # instrument's currency name 2071 "action": action, # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS 2072 "type": orderType, # type of order from TKS_STOP_ORDER_TYPES 2073 "expType": expType, # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES 2074 "createDate": createDate, # string with created order date and time from UTC format (without nano seconds part) 2075 "expDate": expDate, # string with expiration order date and time from UTC format (without nano seconds part) 2076 }) 2077 2078 # --- calculating data for analytics section: 2079 # portfolio distribution by assets: 2080 view["analytics"]["distrByAssets"] = { 2081 "Ruble": { 2082 "uniques": 1, 2083 "cost": view["stat"]["availableRUB"], 2084 "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2085 }, 2086 "Currencies": { 2087 "uniques": len(view["stat"]["Currencies"]), # all foreign currencies without RUB 2088 "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"], 2089 "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2090 }, 2091 "Shares": { 2092 "uniques": len(view["stat"]["Shares"]), 2093 "cost": view["stat"]["sharesCostRUB"], 2094 "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2095 }, 2096 "Bonds": { 2097 "uniques": len(view["stat"]["Bonds"]), 2098 "cost": view["stat"]["bondsCostRUB"], 2099 "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2100 }, 2101 "Etfs": { 2102 "uniques": len(view["stat"]["Etfs"]), 2103 "cost": view["stat"]["etfsCostRUB"], 2104 "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2105 }, 2106 "Futures": { 2107 "uniques": len(view["stat"]["Futures"]), 2108 "cost": view["stat"]["futuresCostRUB"], 2109 "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2110 }, 2111 } 2112 2113 # portfolio distribution by companies: 2114 view["analytics"]["distrByCompanies"]["All money cash"] = { 2115 "ticker": "", 2116 "cost": view["stat"]["allCurrenciesCostRUB"], 2117 "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2118 } 2119 view["analytics"]["distrByCompanies"].update(byComp) 2120 2121 # portfolio distribution by sectors: 2122 view["analytics"]["distrBySectors"]["All money cash"] = { 2123 "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"], 2124 "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"], 2125 } 2126 view["analytics"]["distrBySectors"].update(bySect) 2127 2128 # portfolio distribution by currencies: 2129 if "rub" not in view["analytics"]["distrByCurrencies"].keys(): 2130 view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0} 2131 2132 if self.moreDebug: 2133 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!") 2134 2135 view["analytics"]["distrByCurrencies"].update(byCurr) 2136 view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2137 view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2138 2139 # portfolio distribution by countries: 2140 if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys(): 2141 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0} 2142 2143 if self.moreDebug: 2144 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!") 2145 2146 view["analytics"]["distrByCountries"].update(byCountry) 2147 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2148 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2149 2150 # --- Prepare text statistics overview in human-readable: 2151 if show or onlyFiles: 2152 actualOnDate = datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) 2153 2154 # Whatever the value `details`, header not changes: 2155 info = [ 2156 "# Client's portfolio\n\n", 2157 "* **Actual on date:** [{} UTC]\n".format(actualOnDate), 2158 "* **Account ID:** [{}]\n".format(self.accountId), 2159 ] 2160 2161 if details in ["full", "positions", "digest"]: 2162 info.extend([ 2163 "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2164 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format( 2165 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2166 view["stat"]["totalChangesRUB"], 2167 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2168 view["stat"]["totalChangesPercentRUB"], 2169 ), 2170 ]) 2171 2172 if details in ["full", "positions"]: 2173 info.extend([ 2174 "## Open positions\n\n", 2175 "| Ticker [FIGI] | Volume (blocked) | Lots | Curr. price | Avg. price | Current volume cost | Profit (%) |\n", 2176 "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n", 2177 "| **Ruble:** | {:>31} | | | | | |\n".format( 2178 "{:.2f} ({:.2f}) rub".format( 2179 view["stat"]["availableRUB"], 2180 view["stat"]["blockedRUB"], 2181 ) 2182 ) 2183 ]) 2184 2185 def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list: 2186 return [ 2187 "| | | | | | | |\n", 2188 "| {:<27} | | | | | {:>19} | |\n".format( 2189 noTradeStr if noTradeStr else typeStr, 2190 "" if noTradeStr else "{:.2f} RUB".format(CostRUB), 2191 ), 2192 ] 2193 2194 def _InfoStr(data: dict, isCurr: bool = False) -> str: 2195 return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format( 2196 "{} [{}]".format(data["ticker"], data["figi"]), 2197 "{:.2f} ({:.2f}) {}".format( 2198 data["volume"], 2199 data["blocked"], 2200 data["currency"], 2201 ) if isCurr else "{:.0f} ({:.0f})".format( 2202 data["volume"], 2203 data["blocked"], 2204 ), 2205 "—" if isCurr else "{:.4f}".format(data["lots"]).rstrip("0").rstrip("."), 2206 "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a", 2207 "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a", 2208 "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]), 2209 "{}{:.2f} {} ({}{:.2f}%)".format( 2210 "+" if data["profit"] > 0 else "", 2211 data["profit"], data["baseCurrencyName"], 2212 "+" if data["percentProfit"] > 0 else "", 2213 data["percentProfit"], 2214 ), 2215 ) 2216 2217 # --- Show currencies section: 2218 if view["stat"]["Currencies"]: 2219 info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**")) 2220 for item in view["stat"]["Currencies"]: 2221 info.append(_InfoStr(item, isCurr=True)) 2222 2223 else: 2224 info.extend(_SplitStr(noTradeStr="**Currencies:** no trades")) 2225 2226 # --- Show shares section: 2227 if view["stat"]["Shares"]: 2228 info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**")) 2229 2230 for item in view["stat"]["Shares"]: 2231 info.append(_InfoStr(item)) 2232 2233 else: 2234 info.extend(_SplitStr(noTradeStr="**Shares:** no trades")) 2235 2236 # --- Show bonds section: 2237 if view["stat"]["Bonds"]: 2238 info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**")) 2239 2240 for item in view["stat"]["Bonds"]: 2241 info.append(_InfoStr(item)) 2242 2243 else: 2244 info.extend(_SplitStr(noTradeStr="**Bonds:** no trades")) 2245 2246 # --- Show etfs section: 2247 if view["stat"]["Etfs"]: 2248 info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**")) 2249 2250 for item in view["stat"]["Etfs"]: 2251 info.append(_InfoStr(item)) 2252 2253 else: 2254 info.extend(_SplitStr(noTradeStr="**Etfs:** no trades")) 2255 2256 # --- Show futures section: 2257 if view["stat"]["Futures"]: 2258 info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**")) 2259 2260 for item in view["stat"]["Futures"]: 2261 info.append(_InfoStr(item)) 2262 2263 else: 2264 info.extend(_SplitStr(noTradeStr="**Futures:** no trades")) 2265 2266 if details in ["full", "orders"]: 2267 # --- Show pending limit orders section: 2268 if view["stat"]["orders"]: 2269 info.extend([ 2270 "\n## Opened pending limit-orders: [{}]\n".format(len(view["stat"]["orders"])), 2271 "\n| Ticker [FIGI] | Order ID | Lots (exec.) | Current price (% delta) | Target price | Action | Type | Create date (UTC) |\n", 2272 "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n", 2273 ]) 2274 2275 for item in view["stat"]["orders"]: 2276 info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format( 2277 "{} [{}]".format(item["ticker"], item["figi"]), 2278 item["orderID"], 2279 "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]), 2280 "{} {} ({}{:.2f}%)".format( 2281 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2282 item["baseCurrencyName"], 2283 "+" if item["percentChanges"] > 0 else "", 2284 float(item["percentChanges"]), 2285 ), 2286 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2287 item["action"], 2288 item["type"], 2289 item["date"], 2290 )) 2291 2292 else: 2293 info.append("\n## Total pending limit-orders: [0]\n") 2294 2295 # --- Show stop orders section: 2296 if view["stat"]["stopOrders"]: 2297 info.extend([ 2298 "\n## Opened stop-orders: [{}]\n".format(len(view["stat"]["stopOrders"])), 2299 "\n| Ticker [FIGI] | Stop order ID | Lots | Current price (% delta) | Target price | Limit price | Action | Type | Expire type | Create date (UTC) | Expiration (UTC) |\n", 2300 "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n", 2301 ]) 2302 2303 for item in view["stat"]["stopOrders"]: 2304 info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format( 2305 "{} [{}]".format(item["ticker"], item["figi"]), 2306 item["orderID"], 2307 item["lotsRequested"], 2308 "{} {} ({}{:.2f}%)".format( 2309 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2310 item["baseCurrencyName"], 2311 "+" if item["percentChanges"] > 0 else "", 2312 float(item["percentChanges"]), 2313 ), 2314 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2315 "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"], 2316 item["action"], 2317 item["type"], 2318 item["expType"], 2319 item["createDate"], 2320 item["expDate"], 2321 )) 2322 2323 else: 2324 info.append("\n## Total stop-orders: [0]\n") 2325 2326 if details in ["full", "analytics"]: 2327 # -- Show analytics section: 2328 if view["stat"]["portfolioCostRUB"] > 0: 2329 info.extend([ 2330 "\n# Analytics\n\n" 2331 "* **Actual on date:** [{} UTC]\n".format(actualOnDate), 2332 "* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2333 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format( 2334 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2335 view["stat"]["totalChangesRUB"], 2336 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2337 view["stat"]["totalChangesPercentRUB"], 2338 ), 2339 "\n## Portfolio distribution by assets\n" 2340 "\n| Type | Uniques | Percent | Current cost |\n", 2341 "|------------------------------------|---------|---------|--------------------|\n", 2342 ]) 2343 2344 for key in view["analytics"]["distrByAssets"].keys(): 2345 if view["analytics"]["distrByAssets"][key]["cost"] > 0: 2346 info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format( 2347 key, 2348 view["analytics"]["distrByAssets"][key]["uniques"], 2349 "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]), 2350 "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]), 2351 )) 2352 2353 aSepLine = "|----------------------------------------------|---------|--------------------|\n" 2354 2355 info.extend([ 2356 "\n## Portfolio distribution by companies\n" 2357 "\n| Company | Percent | Current cost |\n", 2358 aSepLine, 2359 ]) 2360 2361 for company in view["analytics"]["distrByCompanies"].keys(): 2362 if view["analytics"]["distrByCompanies"][company]["cost"] > 0: 2363 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2364 "{}{}".format( 2365 "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "", 2366 company, 2367 ), 2368 "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]), 2369 "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]), 2370 )) 2371 2372 info.extend([ 2373 "\n## Portfolio distribution by sectors\n" 2374 "\n| Sector | Percent | Current cost |\n", 2375 aSepLine, 2376 ]) 2377 2378 for sector in view["analytics"]["distrBySectors"].keys(): 2379 if view["analytics"]["distrBySectors"][sector]["cost"] > 0: 2380 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2381 sector, 2382 "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]), 2383 "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]), 2384 )) 2385 2386 info.extend([ 2387 "\n## Portfolio distribution by currencies\n" 2388 "\n| Instruments currencies | Percent | Current cost |\n", 2389 aSepLine, 2390 ]) 2391 2392 for curr in view["analytics"]["distrByCurrencies"].keys(): 2393 if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0: 2394 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2395 "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]), 2396 "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]), 2397 "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]), 2398 )) 2399 2400 info.extend([ 2401 "\n## Portfolio distribution by countries\n" 2402 "\n| Assets by country | Percent | Current cost |\n", 2403 aSepLine, 2404 ]) 2405 2406 for country in view["analytics"]["distrByCountries"].keys(): 2407 if view["analytics"]["distrByCountries"][country]["cost"] > 0: 2408 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2409 country, 2410 "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]), 2411 "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]), 2412 )) 2413 2414 if details in ["full", "calendar"]: 2415 # -- Show bonds payment calendar section: 2416 if view["stat"]["Bonds"]: 2417 bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]] 2418 view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False) 2419 info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False)) 2420 2421 else: 2422 info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n") 2423 2424 infoText = "".join(info) 2425 2426 if show and not onlyFiles: 2427 uLogger.info(infoText) 2428 2429 if details == "full" and self.overviewFile: 2430 filename = self.overviewFile 2431 2432 elif details == "digest" and self.overviewDigestFile: 2433 filename = self.overviewDigestFile 2434 2435 elif details == "positions" and self.overviewPositionsFile: 2436 filename = self.overviewPositionsFile 2437 2438 elif details == "orders" and self.overviewOrdersFile: 2439 filename = self.overviewOrdersFile 2440 2441 elif details == "analytics" and self.overviewAnalyticsFile: 2442 filename = self.overviewAnalyticsFile 2443 2444 elif details == "calendar" and self.overviewBondsCalendarFile: 2445 filename = self.overviewBondsCalendarFile 2446 2447 else: 2448 filename = "" 2449 2450 if filename and (show or onlyFiles): 2451 with open(filename, "w", encoding="UTF-8") as fH: 2452 fH.write(infoText) 2453 2454 uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename))) 2455 2456 if self.useHTMLReports: 2457 htmlFilePath = filename.replace(".md", ".html") if filename.endswith(".md") else filename + ".html" 2458 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 2459 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's portfolio", commonCSS=COMMON_CSS, markdown=infoText)) 2460 2461 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 2462 2463 return view 2464 2465 def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True, onlyFiles=False) -> tuple[list[dict], dict]: 2466 """ 2467 Returns history operations between two given dates for current `accountId`. 2468 If `reportFile` string is not empty then also save human-readable report. 2469 Shows some statistical data of closed positions. 2470 2471 :param start: see docstring in `TradeRoutines.GetDatesAsString()` method. 2472 :param end: see docstring in `TradeRoutines.GetDatesAsString()` method. 2473 :param show: if `True` then also prints all records to the console. 2474 :param showCancelled: if `False` then remove information about cancelled operations from the deals report. 2475 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 2476 :return: original list of dictionaries with history of deals records from API ("operations" key): 2477 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2478 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc. 2479 """ 2480 if self.accountId is None or not self.accountId: 2481 uLogger.error("Variable `accountId` must be defined for using this method!") 2482 raise Exception("Account ID required") 2483 2484 startDate, endDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT) # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2485 2486 uLogger.debug("Requesting history of a client's operations. Wait, please...") 2487 2488 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2489 dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations" 2490 self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate}) 2491 ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"] # list of dict: operations returns by broker 2492 customStat = {} # custom statistics in additional to responseJSON 2493 2494 # --- output report in human-readable format: 2495 if self.reportFile and (show or onlyFiles): 2496 splitLine1 = "| | | | | |\n" # Summary section 2497 splitLine2 = "| | | | | | | | |\n" # Operations section 2498 nextDay = "" 2499 2500 info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])] 2501 2502 if len(ops) > 0: 2503 customStat = { 2504 "opsCount": 0, # total operations count 2505 "buyCount": 0, # buy operations 2506 "sellCount": 0, # sell operations 2507 "buyTotal": {"rub": 0.}, # Buy sums in different currencies 2508 "sellTotal": {"rub": 0.}, # Sell sums in different currencies 2509 "payIn": {"rub": 0.}, # Deposit brokerage account 2510 "payOut": {"rub": 0.}, # Withdrawals 2511 "divs": {"rub": 0.}, # Dividends income 2512 "coupons": {"rub": 0.}, # Coupon's income 2513 "brokerCom": {"rub": 0.}, # Service commissions 2514 "serviceCom": {"rub": 0.}, # Service commissions 2515 "marginCom": {"rub": 0.}, # Margin commissions 2516 "allTaxes": {"rub": 0.}, # Sum of withholding taxes and corrections 2517 } 2518 2519 # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES: 2520 for item in ops: 2521 if item["state"] == "OPERATION_STATE_EXECUTED": 2522 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2523 2524 # count buy operations: 2525 if "_BUY" in item["operationType"]: 2526 customStat["buyCount"] += 1 2527 2528 if item["payment"]["currency"] in customStat["buyTotal"].keys(): 2529 customStat["buyTotal"][item["payment"]["currency"]] += payment 2530 2531 else: 2532 customStat["buyTotal"][item["payment"]["currency"]] = payment 2533 2534 # count sell operations: 2535 elif "_SELL" in item["operationType"]: 2536 customStat["sellCount"] += 1 2537 2538 if item["payment"]["currency"] in customStat["sellTotal"].keys(): 2539 customStat["sellTotal"][item["payment"]["currency"]] += payment 2540 2541 else: 2542 customStat["sellTotal"][item["payment"]["currency"]] = payment 2543 2544 # count incoming operations: 2545 elif item["operationType"] in ["OPERATION_TYPE_INPUT"]: 2546 if item["payment"]["currency"] in customStat["payIn"].keys(): 2547 customStat["payIn"][item["payment"]["currency"]] += payment 2548 2549 else: 2550 customStat["payIn"][item["payment"]["currency"]] = payment 2551 2552 # count withdrawals operations: 2553 elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]: 2554 if item["payment"]["currency"] in customStat["payOut"].keys(): 2555 customStat["payOut"][item["payment"]["currency"]] += payment 2556 2557 else: 2558 customStat["payOut"][item["payment"]["currency"]] = payment 2559 2560 # count dividends income: 2561 elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]: 2562 if item["payment"]["currency"] in customStat["divs"].keys(): 2563 customStat["divs"][item["payment"]["currency"]] += payment 2564 2565 else: 2566 customStat["divs"][item["payment"]["currency"]] = payment 2567 2568 # count coupon's income: 2569 elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]: 2570 if item["payment"]["currency"] in customStat["coupons"].keys(): 2571 customStat["coupons"][item["payment"]["currency"]] += payment 2572 2573 else: 2574 customStat["coupons"][item["payment"]["currency"]] = payment 2575 2576 # count broker commissions: 2577 elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]: 2578 if item["payment"]["currency"] in customStat["brokerCom"].keys(): 2579 customStat["brokerCom"][item["payment"]["currency"]] += payment 2580 2581 else: 2582 customStat["brokerCom"][item["payment"]["currency"]] = payment 2583 2584 # count service commissions: 2585 elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]: 2586 if item["payment"]["currency"] in customStat["serviceCom"].keys(): 2587 customStat["serviceCom"][item["payment"]["currency"]] += payment 2588 2589 else: 2590 customStat["serviceCom"][item["payment"]["currency"]] = payment 2591 2592 # count margin commissions: 2593 elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]: 2594 if item["payment"]["currency"] in customStat["marginCom"].keys(): 2595 customStat["marginCom"][item["payment"]["currency"]] += payment 2596 2597 else: 2598 customStat["marginCom"][item["payment"]["currency"]] = payment 2599 2600 # count withholding taxes: 2601 elif "_TAX" in item["operationType"]: 2602 if item["payment"]["currency"] in customStat["allTaxes"].keys(): 2603 customStat["allTaxes"][item["payment"]["currency"]] += payment 2604 2605 else: 2606 customStat["allTaxes"][item["payment"]["currency"]] = payment 2607 2608 else: 2609 continue 2610 2611 customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"] 2612 2613 # --- view "Actions" lines: 2614 info.extend([ 2615 "| Report sections | | | | |\n", 2616 "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n", 2617 "| **Actions:** | Trades: {:<21} | Trading volumes: | | |\n".format(customStat["opsCount"]), 2618 "| | Buy: {:<22} | {:<28} | | |\n".format( 2619 "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2620 " rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else " —", 2621 ), 2622 "| | Sell: {:<21} | {:<28} | | |\n".format( 2623 "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2624 " rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else " —", 2625 ), 2626 ]) 2627 2628 opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys())))) 2629 for key in opsKeys: 2630 if key == "rub": 2631 continue 2632 2633 info.extend([ 2634 "| | | {:<28} | | |\n".format( 2635 " {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0) 2636 ), 2637 "| | | {:<28} | | |\n".format( 2638 " {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0) 2639 ), 2640 ]) 2641 2642 info.append(splitLine1) 2643 2644 def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str: 2645 return "| | {:<29} | {:<28} | {:<20} | {:<22} |\n".format( 2646 " {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else " —", 2647 " {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else " —", 2648 " {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else " —", 2649 " {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else " —", 2650 ) 2651 2652 # --- view "Payments" lines: 2653 info.append("| **Payments:** | Deposit on broker account: | Withdrawals: | Dividends income: | Coupons income: |\n") 2654 paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys())))) 2655 2656 for key in paymentsKeys: 2657 info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key)) 2658 2659 info.append(splitLine1) 2660 2661 # --- view "Commissions and taxes" lines: 2662 info.append("| **Commissions and taxes:** | Broker commissions: | Service commissions: | Margin commissions: | All taxes/corrections: |\n") 2663 comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys())))) 2664 2665 for key in comKeys: 2666 info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key)) 2667 2668 info.extend([ 2669 "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"), 2670 "| Date and time | FIGI | Ticker | Asset | Value | Payment | Status | Operation type |\n", 2671 "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n", 2672 ]) 2673 2674 else: 2675 info.append("Broker returned no operations during this period\n") 2676 2677 # --- view "Operations" section: 2678 for item in ops: 2679 if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]: 2680 continue 2681 2682 else: 2683 self._figi = item["figi"] 2684 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2685 instrument = self.SearchByFIGI(requestPrice=False) if self._figi else {} 2686 2687 # group of deals during one day: 2688 if nextDay and item["date"].split("T")[0] != nextDay: 2689 info.append(splitLine2) 2690 nextDay = "" 2691 2692 else: 2693 nextDay = item["date"].split("T")[0] # saving current day for splitting 2694 2695 info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format( 2696 item["date"].replace("T", " ").replace("Z", "").split(".")[0], 2697 self._figi if self._figi else "—", 2698 instrument["ticker"] if instrument else "—", 2699 instrument["type"] if instrument else "—", 2700 item["quantity"] if int(item["quantity"]) > 0 else "—", 2701 "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—", 2702 TKS_OPERATION_STATES[item["state"]], 2703 TKS_OPERATION_TYPES[item["operationType"]], 2704 )) 2705 2706 infoText = "".join(info) 2707 2708 if show and not onlyFiles: 2709 if self.moreDebug: 2710 uLogger.debug("Records about history of a client's operations successfully received") 2711 2712 uLogger.info(infoText) 2713 2714 if self.reportFile and (show or onlyFiles): 2715 with open(self.reportFile, "w", encoding="UTF-8") as fH: 2716 fH.write(infoText) 2717 2718 uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile))) 2719 2720 if self.useHTMLReports: 2721 htmlFilePath = self.reportFile.replace(".md", ".html") if self.reportFile.endswith(".md") else self.reportFile + ".html" 2722 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 2723 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's operations", commonCSS=COMMON_CSS, markdown=infoText)) 2724 2725 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 2726 2727 return ops, customStat 2728 2729 def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False, onlyFiles=False) -> pd.DataFrame: 2730 """ 2731 This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id). 2732 2733 History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. 2734 Warning! Broker server used ISO UTC time by default. 2735 2736 If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame. 2737 Also, `historyFile` used to update history with `onlyMissing` parameter. 2738 2739 See also: `LoadHistory()` and `ShowHistoryChart()` methods. 2740 2741 :param start: see docstring in `TradeRoutines.GetDatesAsString()` method. 2742 :param end: see docstring in `TradeRoutines.GetDatesAsString()` method. 2743 :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`, 2744 `"hour"`, `"day"`. Default: `"hour"`. 2745 :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`. 2746 False by default. Warning! History appends only from last candle to current time 2747 with always update last candle! 2748 :param csvSep: separator if csv-file is used, `,` by default. 2749 :param show: if `True` then also prints Pandas DataFrame to the console. 2750 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 2751 :return: Pandas DataFrame with prices history. Headers of columns are defined by default: 2752 `["date", "time", "open", "high", "low", "close", "volume"]`. 2753 """ 2754 strStartDate, strEndDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT) # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2755 headers = ["date", "time", "open", "high", "low", "close", "volume"] # sequence and names of column headers 2756 history = None # empty pandas object for history 2757 2758 if interval not in TKS_CANDLE_INTERVALS.keys(): 2759 uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.") 2760 raise Exception("Incorrect value") 2761 2762 if not (self._ticker or self._figi): 2763 uLogger.error("Ticker or FIGI must be defined!") 2764 raise Exception("Ticker or FIGI required") 2765 2766 if self._ticker and not self._figi: 2767 instrumentByTicker = self.SearchByTicker(requestPrice=False) 2768 self._figi = instrumentByTicker["figi"] if instrumentByTicker else "" 2769 2770 if self._figi and not self._ticker: 2771 instrumentByFIGI = self.SearchByFIGI(requestPrice=False) 2772 self._ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else "" 2773 2774 dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from start time string 2775 dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from end time string 2776 if interval.lower() != "day": 2777 dtEnd += timedelta(seconds=1) # adds 1 sec for requests, because day end returned by `TradeRoutines.GetDatesAsString()` is 23:59:59 2778 2779 delta = dtEnd - dtStart # current UTC time minus last time in file 2780 deltaMinutes = delta.days * 1440 + delta.seconds // 60 # minutes between start and end dates 2781 2782 # calculate history length in candles: 2783 length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1] 2784 if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0: 2785 length += 1 # to avoid fraction time 2786 2787 # calculate data blocks count: 2788 blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2] 2789 2790 uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self._ticker, self._figi)) 2791 if self.moreDebug: 2792 uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end)) 2793 uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate)) 2794 uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval)) 2795 uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2])) 2796 2797 tempOld = None # pandas object for old history, if --only-missing key present 2798 lastTime = None # datetime object of last old candle in file 2799 2800 if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile): 2801 if self.moreDebug: 2802 uLogger.debug("--only-missing key present, add only last missing candles...") 2803 uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile))) 2804 2805 tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers) 2806 2807 tempOld["date"] = pd.to_datetime(tempOld["date"]) # load date "as is" 2808 tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d") # convert date to string 2809 tempOld["time"] = pd.to_datetime(tempOld["time"]) # load time "as is" 2810 tempOld["time"] = tempOld["time"].dt.strftime("%H:%M") # convert time to string 2811 2812 # get last datetime object from last string in file or minus 1 delta if file is empty: 2813 if len(tempOld) > 0: 2814 lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2815 2816 else: 2817 lastTime = dtEnd - timedelta(days=1) # history file is empty, so last date set at -1 day 2818 2819 tempOld = tempOld[:-1] # always remove last old candle because it may be incompletely at the current time 2820 2821 responseJSONs = [] # raw history blocks of data 2822 2823 blockEnd = dtEnd 2824 for item in range(blocks): 2825 tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2] 2826 blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail) 2827 2828 uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format( 2829 item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2830 )) 2831 2832 if blockStart == blockEnd: 2833 uLogger.debug("Skipped this zero-length block...") 2834 2835 else: 2836 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles 2837 historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles" 2838 self.body = str({ 2839 "figi": self._figi, 2840 "from": blockStart.strftime(TKS_DATE_TIME_FORMAT), 2841 "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2842 "interval": TKS_CANDLE_INTERVALS[interval][0] 2843 }) 2844 responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1) 2845 2846 if "code" in responseJSON.keys(): 2847 uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks)) 2848 2849 else: 2850 if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1: 2851 responseJSON["candles"] = responseJSON["candles"][:-1] # removes last candle for "yesterday" request 2852 2853 responseJSONs = responseJSON["candles"] + responseJSONs # add more old history behind newest dates 2854 2855 blockEnd = blockStart 2856 2857 printCount = len(responseJSONs) # candles to show in console 2858 if responseJSONs: 2859 tempHistory = pd.DataFrame( 2860 data={ 2861 "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2862 "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2863 "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs], 2864 "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs], 2865 "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs], 2866 "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs], 2867 "volume": [int(item["volume"]) for item in responseJSONs], 2868 }, 2869 index=range(len(responseJSONs)), 2870 columns=["date", "time", "open", "high", "low", "close", "volume"], 2871 ) 2872 tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d") 2873 tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M") 2874 2875 # append only newest candles to old history if --only-missing key present: 2876 if onlyMissing and tempOld is not None and lastTime is not None: 2877 index = 0 # find start index in tempHistory data: 2878 2879 for i, item in tempHistory.iterrows(): 2880 curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2881 2882 if curTime == lastTime: 2883 uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 2884 index = i 2885 printCount = index + 1 2886 break 2887 2888 history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True) 2889 2890 else: 2891 history = tempHistory # if no `--only-missing` key then load full data from server 2892 2893 if self.moreDebug: 2894 uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False))) 2895 2896 if history is not None and not history.empty: 2897 if show and not onlyFiles: 2898 uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format( 2899 strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]), 2900 pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False), 2901 )) 2902 2903 else: 2904 uLogger.warning("Received an empty candles history!") 2905 2906 if self.historyFile is not None: 2907 if history is not None and not history.empty: 2908 history.to_csv(self.historyFile, sep=csvSep, index=False, header=False) 2909 uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self._ticker, self._figi, interval, os.path.abspath(self.historyFile))) 2910 2911 else: 2912 uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile))) 2913 2914 else: 2915 if self.moreDebug: 2916 uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.") 2917 2918 return history 2919 2920 def LoadHistory(self, filePath: str) -> pd.DataFrame: 2921 """ 2922 Load candles history from csv-file and return Pandas DataFrame object. 2923 2924 See also: `History()` and `ShowHistoryChart()` methods. 2925 2926 :param filePath: path to csv-file to open. 2927 """ 2928 loadedHistory = None # init candles data object 2929 2930 uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...") 2931 2932 if os.path.exists(filePath): 2933 loadedHistory = self.priceModel.LoadFromFile(filePath) # load data and get chain of candles as Pandas DataFrame 2934 2935 tfStr = self.priceModel.FormattedDelta( 2936 self.priceModel.timeframe, 2937 "{days} days {hours}h {minutes}m {seconds}s", 2938 ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta( 2939 self.priceModel.timeframe, 2940 "{hours}h {minutes}m {seconds}s", 2941 ) 2942 2943 if loadedHistory is not None and not loadedHistory.empty: 2944 uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format( 2945 len(loadedHistory), 2946 tfStr, 2947 pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)), 2948 ) 2949 2950 else: 2951 uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath))) 2952 2953 else: 2954 uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath)) 2955 2956 return loadedHistory 2957 2958 def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None: 2959 """ 2960 Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file. 2961 2962 Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart. 2963 Default: `index.html` (both for interact and non-interact candlesticks chart). 2964 2965 See also: `History()` and `LoadHistory()` methods. 2966 2967 :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object. 2968 :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. 2969 See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters 2970 If False then chain of candlesticks will render as not interactive Google Candlestick chart. 2971 See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template 2972 :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to 2973 html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file. 2974 """ 2975 if isinstance(candles, str): 2976 self.priceModel.prices = self.LoadHistory(filePath=candles) # load candles chain from file 2977 self.priceModel.ticker = os.path.basename(candles) # use filename as ticker name in PriceGenerator 2978 2979 elif isinstance(candles, pd.DataFrame): 2980 self.priceModel.prices = candles # set candles chain from variable 2981 self.priceModel.ticker = self._ticker # use current TKSBrokerAPI ticker as ticker name in PriceGenerator 2982 2983 if "datetime" not in candles.columns: 2984 self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True) # PriceGenerator uses "datetime" column with date and time 2985 2986 else: 2987 uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!") 2988 raise Exception("Incorrect value") 2989 2990 self.priceModel.horizon = len(self.priceModel.prices) # use length of candles data as horizon in PriceGenerator 2991 2992 if interact: 2993 uLogger.debug("Rendering interactive candles chart. Wait, please...") 2994 2995 self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2996 2997 else: 2998 uLogger.debug("Rendering non-interactive candles chart. Wait, please...") 2999 3000 self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 3001 3002 uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile))) 3003 3004 def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 3005 """ 3006 Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response. 3007 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 3008 3009 See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`. 3010 3011 :param operation: string "Buy" or "Sell". 3012 :param lots: volume, integer count of lots >= 1. 3013 :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`. 3014 :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`. 3015 :param expDate: string "Undefined" by default or local date in future, 3016 it is a string with format `%Y-%m-%d %H:%M:%S`. 3017 :return: JSON with response from broker server. 3018 """ 3019 if self.accountId is None or not self.accountId: 3020 uLogger.error("Variable `accountId` must be defined for using this method!") 3021 raise Exception("Account ID required") 3022 3023 if operation is None or not operation or operation not in ("Buy", "Sell"): 3024 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 3025 raise Exception("Incorrect value") 3026 3027 if lots is None or lots < 1: 3028 uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.") 3029 lots = 1 3030 3031 if tp is None or tp < 0: 3032 tp = 0 3033 3034 if sl is None or sl < 0: 3035 sl = 0 3036 3037 if expDate is None or not expDate: 3038 expDate = "Undefined" 3039 3040 if not (self._ticker or self._figi): 3041 uLogger.error("Ticker or FIGI must be defined!") 3042 raise Exception("Ticker or FIGI required") 3043 3044 instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True) 3045 self._ticker = instrument["ticker"] 3046 self._figi = instrument["figi"] 3047 3048 uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self._ticker, self._figi, lots, tp, sl, expDate)) 3049 3050 openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3051 self.body = str({ 3052 "figi": self._figi, 3053 "quantity": str(lots), 3054 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3055 "accountId": str(self.accountId), 3056 "orderType": "ORDER_TYPE_MARKET", # see: TKS_ORDER_TYPES 3057 }) 3058 response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0) 3059 3060 if "orderId" in response.keys(): 3061 uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format( 3062 operation, response["orderId"], 3063 self._ticker, self._figi, lots, 3064 NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"], 3065 NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"], 3066 NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"], 3067 )) 3068 3069 if tp > 0: 3070 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate) 3071 3072 if sl > 0: 3073 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate) 3074 3075 else: 3076 uLogger.warning("Not `oK` status received! Market order not executed. See full debug log and try again open order later.") 3077 3078 return response 3079 3080 def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 3081 """ 3082 More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response. 3083 If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter. 3084 3085 See also: `Order()` and `Trade()` docstrings. 3086 3087 :param lots: volume, integer count of lots >= 1. 3088 :param tp: float > 0, take profit price of stop-order. 3089 :param sl: float > 0, stop loss price of stop-order. 3090 :param expDate: it's a local date in future. 3091 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3092 :return: JSON with response from broker server. 3093 """ 3094 return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate) 3095 3096 def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 3097 """ 3098 More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response. 3099 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 3100 3101 See also: `Order()` and `Trade()` docstrings. 3102 3103 :param lots: volume, integer count of lots >= 1. 3104 :param tp: float > 0, take profit price of stop-order. 3105 :param sl: float > 0, stop loss price of stop-order. 3106 :param expDate: it's a local date in the future. 3107 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3108 :return: JSON with response from broker server. 3109 """ 3110 return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate) 3111 3112 def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None: 3113 """ 3114 Close position of given instruments. 3115 3116 :param instruments: list of instruments defined by tickers or FIGIs that must be closed. 3117 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3118 This avoids unnecessary downloading data from the server. 3119 """ 3120 if instruments is None or not instruments: 3121 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 3122 raise Exception("Ticker or FIGI required") 3123 3124 if isinstance(instruments, str): 3125 instruments = [instruments] 3126 3127 uniqueInstruments = self.GetUniqueFIGIs(instruments) 3128 if uniqueInstruments: 3129 if portfolio is None or not portfolio: 3130 portfolio = self.Overview(show=False) 3131 3132 allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]] 3133 uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened))) 3134 3135 for self._figi in uniqueInstruments: 3136 if self._figi not in allOpened: 3137 uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self._figi)) 3138 continue 3139 3140 # search open trade info about instrument by ticker: 3141 instrument = {} 3142 for iType in TKS_INSTRUMENTS: 3143 if instrument: 3144 break 3145 3146 for item in portfolio["stat"][iType]: 3147 if item["figi"] == self._figi: 3148 instrument = item 3149 break 3150 3151 if instrument: 3152 self._ticker = instrument["ticker"] 3153 self._figi = instrument["figi"] 3154 3155 uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format( 3156 self._ticker, 3157 self._figi, 3158 int(instrument["volume"]), 3159 ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "", 3160 )) 3161 3162 tradeLots = abs(instrument["lots"]) - instrument["blocked"] # available volumes in lots for close operation 3163 3164 if tradeLots > 0: 3165 if instrument["blocked"] > 0: 3166 uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format( 3167 instrument["blocked"], 3168 self._ticker, 3169 tradeLots, 3170 )) 3171 3172 # if direction is "Long" then we need sell, if direction is "Short" then we need buy: 3173 self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots) 3174 3175 else: 3176 uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self._ticker)) 3177 3178 def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None: 3179 """ 3180 Close all positions of given instruments with defined type. 3181 3182 :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list. 3183 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3184 This avoids unnecessary downloading data from the server. 3185 """ 3186 if iType not in TKS_INSTRUMENTS: 3187 uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType)) 3188 3189 else: 3190 if portfolio is None or not portfolio: 3191 portfolio = self.Overview(show=False) 3192 3193 tickers = [item["ticker"] for item in portfolio["stat"][iType]] 3194 uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers)) 3195 3196 if tickers and portfolio: 3197 self.CloseTrades(tickers, portfolio) 3198 3199 else: 3200 uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType)) 3201 3202 def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3203 """ 3204 Universal method to create market or limit orders with all available parameters for current `accountId`. 3205 See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`. 3206 3207 If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above 3208 current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day. 3209 3210 Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" 3211 then broker immediately open market order as you can do simple --buy or --sell operations! 3212 3213 If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". 3214 When current price will go up or down to target price value then broker opens a limit order. 3215 Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter. 3216 3217 Only one attempt and no retry for opens order. If network issue occurred you can create new request. 3218 3219 :param operation: string "Buy" or "Sell". 3220 :param orderType: string "Limit" or "Stop". 3221 :param lots: volume, integer count of lots >= 1. 3222 :param targetPrice: target price > 0. This is open trade price for limit order. 3223 :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. 3224 Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order. 3225 :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types 3226 "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3227 Stop loss order always executed by market price. 3228 :param expDate: string "Undefined" by default or local date in future. 3229 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3230 This date is converting to UTC format for server. This parameter only makes sense for stop-order. 3231 A limit order has no expiration date, it lasts until the end of the trading day. 3232 :return: JSON with response from broker server. 3233 """ 3234 if self.accountId is None or not self.accountId: 3235 uLogger.error("Variable `accountId` must be defined for using this method!") 3236 raise Exception("Account ID required") 3237 3238 if operation is None or not operation or operation not in ("Buy", "Sell"): 3239 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 3240 raise Exception("Incorrect value") 3241 3242 if orderType is None or not orderType or orderType not in ("Limit", "Stop"): 3243 uLogger.error("You must define order type only one of them: `Limit` or `Stop`!") 3244 raise Exception("Incorrect value") 3245 3246 if lots is None or lots < 1: 3247 uLogger.error("You must define trade volume > 0: integer count of lots!") 3248 raise Exception("Incorrect value") 3249 3250 if targetPrice is None or targetPrice <= 0: 3251 uLogger.error("Target price for limit-order must be greater than 0!") 3252 raise Exception("Incorrect value") 3253 3254 if limitPrice is None or limitPrice <= 0: 3255 limitPrice = targetPrice 3256 3257 if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"): 3258 stopType = "Limit" 3259 3260 if expDate is None or not expDate: 3261 expDate = "Undefined" 3262 3263 if not (self._ticker or self._figi): 3264 uLogger.error("Tocker or FIGI must be defined!") 3265 raise Exception("Ticker or FIGI required") 3266 3267 response = {} 3268 instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True) 3269 self._ticker = instrument["ticker"] 3270 self._figi = instrument["figi"] 3271 3272 if orderType == "Limit": 3273 uLogger.debug( 3274 "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format( 3275 self._ticker, self._figi, 3276 operation, lots, targetPrice, instrument["currency"], 3277 )) 3278 3279 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3280 self.body = str({ 3281 "figi": self._figi, 3282 "quantity": str(lots), 3283 "price": FloatToNano(targetPrice), 3284 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3285 "accountId": str(self.accountId), 3286 "orderType": "ORDER_TYPE_LIMIT", # see: TKS_ORDER_TYPES 3287 }) 3288 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3289 3290 if "orderId" in response.keys(): 3291 uLogger.info( 3292 "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}]".format( 3293 response["orderId"], self._ticker, self._figi, operation, lots, 3294 "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"], 3295 )) 3296 3297 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3298 if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]: 3299 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format( 3300 targetPrice, instrument["currency"], 3301 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3302 )) 3303 3304 if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]: 3305 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format( 3306 targetPrice, instrument["currency"], 3307 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3308 )) 3309 3310 else: 3311 uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log and try again open order later.") 3312 3313 if orderType == "Stop": 3314 uLogger.debug( 3315 "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format( 3316 self._ticker, self._figi, 3317 operation, lots, 3318 targetPrice, instrument["currency"], 3319 limitPrice, instrument["currency"], 3320 stopType, expDate, 3321 )) 3322 3323 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder" 3324 expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT) 3325 stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT" 3326 3327 body = { 3328 "figi": self._figi, 3329 "quantity": str(lots), 3330 "price": FloatToNano(limitPrice), 3331 "stopPrice": FloatToNano(targetPrice), 3332 "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL", # see: TKS_STOP_ORDER_DIRECTIONS 3333 "accountId": str(self.accountId), 3334 "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL", # see: TKS_STOP_ORDER_EXPIRATION_TYPES 3335 "stopOrderType": stopOrderType, # see: TKS_STOP_ORDER_TYPES 3336 } 3337 3338 if expDateUTC: 3339 body["expireDate"] = expDateUTC 3340 3341 self.body = str(body) 3342 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3343 3344 if "stopOrderId" in response.keys(): 3345 uLogger.info( 3346 "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}], limit price [{} {}], stop-order type [{}] and expiration date [{} UTC]".format( 3347 response["stopOrderId"], self._ticker, self._figi, operation, lots, 3348 "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"], 3349 "{:.4f}".format(limitPrice).rstrip("0").rstrip("."), instrument["currency"], 3350 TKS_STOP_ORDER_TYPES[stopOrderType], 3351 datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"], 3352 )) 3353 3354 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3355 if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3356 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{} {}] is lower than the current price [{} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3357 "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"], 3358 "{:.4f}".format(instrument["currentPrice"]["lastPrice"]).rstrip("0").rstrip("."), instrument["currency"], 3359 )) 3360 3361 if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3362 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{} {}] is higher than the current price [{} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3363 "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"], 3364 "{:.4f}".format(instrument["currentPrice"]["lastPrice"]).rstrip("0").rstrip("."), instrument["currency"], 3365 )) 3366 3367 else: 3368 uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log and try again open order later.") 3369 3370 return response 3371 3372 def BuyLimit(self, lots: int, targetPrice: float) -> dict: 3373 """ 3374 Create pending `Buy` limit-order (below current price). You must specify only 2 parameters: 3375 `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then 3376 broker immediately open `Buy` market order, such as if you do simple `--buy` operation! 3377 See also: `Order()` docstring. 3378 3379 :param lots: volume, integer count of lots >= 1. 3380 :param targetPrice: target price > 0. This is open trade price for limit order. 3381 :return: JSON with response from broker server. 3382 """ 3383 return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice) 3384 3385 def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3386 """ 3387 Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order. 3388 In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3389 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3390 target price value then broker opens a limit order. See also: `Order()` docstring. 3391 3392 :param lots: volume, integer count of lots >= 1. 3393 :param targetPrice: target price > 0. This is trigger price for buy stop-order. 3394 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3395 with price equal to limitPrice, when current price goes to target price of buy stop-order. 3396 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3397 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3398 :param expDate: string "Undefined" by default or local date in future. 3399 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3400 This date is converting to UTC format for server. 3401 :return: JSON with response from broker server. 3402 """ 3403 return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3404 3405 def SellLimit(self, lots: int, targetPrice: float) -> dict: 3406 """ 3407 Create pending `Sell` limit-order (above current price). You must specify only 2 parameters: 3408 `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then 3409 broker immediately open `Sell` market order, such as if you do simple `--sell` operation! 3410 See also: `Order()` docstring. 3411 3412 :param lots: volume, integer count of lots >= 1. 3413 :param targetPrice: target price > 0. This is open trade price for limit order. 3414 :return: JSON with response from broker server. 3415 """ 3416 return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice) 3417 3418 def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3419 """ 3420 Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order. 3421 In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3422 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3423 target price value then broker opens a limit order. See also: `Order()` docstring. 3424 3425 :param lots: volume, integer count of lots >= 1. 3426 :param targetPrice: target price > 0. This is trigger price for sell stop-order. 3427 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3428 with price equal to limitPrice, when current price goes to target price of sell stop-order. 3429 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3430 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3431 :param expDate: string "Undefined" by default or local date in future. 3432 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3433 This date is converting to UTC format for server. 3434 :return: JSON with response from broker server. 3435 """ 3436 return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3437 3438 def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None: 3439 """ 3440 Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`. 3441 3442 :param orderIDs: list of integers with `orderId` or `stopOrderId`. 3443 :param allOrdersIDs: pre-received lists of all active pending limit orders. 3444 This avoids unnecessary downloading data from the server. 3445 :param allStopOrdersIDs: pre-received lists of all active stop orders. 3446 """ 3447 if self.accountId is None or not self.accountId: 3448 uLogger.error("Variable `accountId` must be defined for using this method!") 3449 raise Exception("Account ID required") 3450 3451 if orderIDs: 3452 if allOrdersIDs is None: 3453 rawOrders = self.RequestPendingOrders() 3454 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending limit orders ID 3455 3456 if allStopOrdersIDs is None: 3457 rawStopOrders = self.RequestStopOrders() 3458 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3459 3460 for orderID in orderIDs: 3461 idInPendingOrders = orderID in allOrdersIDs 3462 idInStopOrders = orderID in allStopOrdersIDs 3463 3464 if not (idInPendingOrders or idInStopOrders): 3465 uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID)) 3466 continue 3467 3468 else: 3469 if idInPendingOrders: 3470 uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID)) 3471 3472 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder 3473 self.body = str({"accountId": self.accountId, "orderId": orderID}) 3474 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder" 3475 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3476 3477 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3478 if self.moreDebug: 3479 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3480 3481 uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID)) 3482 3483 else: 3484 uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID)) 3485 3486 elif idInStopOrders: 3487 uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID)) 3488 3489 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder 3490 self.body = str({"accountId": self.accountId, "stopOrderId": orderID}) 3491 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder" 3492 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3493 3494 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3495 if self.moreDebug: 3496 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3497 3498 uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID)) 3499 3500 else: 3501 uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID)) 3502 3503 else: 3504 continue 3505 3506 def CloseAllOrders(self) -> None: 3507 """ 3508 Gets a list of open pending and stop orders and cancel it all. 3509 """ 3510 rawOrders = self.RequestPendingOrders() 3511 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending limit orders ID 3512 lenOrders = len(allOrdersIDs) 3513 3514 rawStopOrders = self.RequestStopOrders() 3515 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3516 lenSOrders = len(allStopOrdersIDs) 3517 3518 if lenOrders > 0 or lenSOrders > 0: 3519 uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders)) 3520 3521 self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs) 3522 3523 else: 3524 uLogger.info("Orders not found, nothing to cancel.") 3525 3526 def CloseAll(self, *args) -> None: 3527 """ 3528 Close all available (not blocked) opened trades and orders. 3529 3530 Also, you can select one or more keywords case-insensitive: 3531 `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type. 3532 3533 Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods. 3534 """ 3535 overview = self.Overview(show=False) # get all open trades info 3536 3537 if len(args) == 0: 3538 uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...") 3539 self.CloseAllOrders() # close all pending and stop orders 3540 3541 for iType in TKS_INSTRUMENTS: 3542 if iType != "Currencies": 3543 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3544 3545 else: 3546 uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args))) 3547 lowerArgs = [x.lower() for x in args] 3548 3549 if "orders" in lowerArgs: 3550 self.CloseAllOrders() # close all pending and stop orders 3551 3552 for iType in TKS_INSTRUMENTS: 3553 if iType.lower() in lowerArgs and iType != "Currencies": 3554 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3555 3556 def CloseAllByTicker(self, instrument: str) -> None: 3557 """ 3558 Close all available (not blocked) opened trades and orders for one instrument defined by its ticker. 3559 3560 This method searches opened trade and orders of instrument throw all portfolio and then use 3561 `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument. 3562 3563 See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`. 3564 3565 :param instrument: string with ticker. 3566 """ 3567 if instrument is None or not instrument: 3568 uLogger.error("Ticker name must be defined for using this method!") 3569 raise Exception("Ticker required") 3570 3571 overview = self.Overview(show=False) # get user portfolio with all open trades info 3572 3573 self._ticker = instrument # try to set instrument as ticker 3574 self._figi = "" 3575 3576 limitAll = [item["orderID"] for item in overview["stat"]["orders"]] # list of all pending limit order IDs 3577 stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]] # list of all stop order IDs 3578 3579 if limitAll and self.IsInLimitOrders(portfolio=overview): 3580 uLogger.debug("Closing all opened pending limit orders for the instrument with ticker [{}]. Wait, please...") 3581 self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3582 3583 if stopAll and self.IsInStopOrders(portfolio=overview): 3584 uLogger.debug("Closing all opened stop orders for the instrument with ticker [{}]. Wait, please...") 3585 self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3586 3587 if self.IsInPortfolio(portfolio=overview): 3588 uLogger.debug("Closing all available (not blocked) opened trade for the instrument with ticker [{}]. Wait, please...") 3589 self.CloseTrades(instruments=[instrument], portfolio=overview) 3590 3591 def CloseAllByFIGI(self, instrument: str) -> None: 3592 """ 3593 Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id. 3594 3595 This method searches opened trade and orders of instrument throw all portfolio and then use 3596 `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument. 3597 3598 See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`. 3599 3600 :param instrument: string with FIGI id. 3601 """ 3602 if instrument is None or not instrument: 3603 uLogger.error("FIGI id must be defined for using this method!") 3604 raise Exception("FIGI required") 3605 3606 overview = self.Overview(show=False) # get user portfolio with all open trades info 3607 3608 self._ticker = "" 3609 self._figi = instrument # try to set instrument as FIGI id 3610 3611 limitAll = [item["orderID"] for item in overview["stat"]["orders"]] # list of all pending limit order IDs 3612 stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]] # list of all stop order IDs 3613 3614 if limitAll and self.IsInLimitOrders(portfolio=overview): 3615 uLogger.debug("Closing all opened pending limit orders for the instrument with FIGI [{}]. Wait, please...") 3616 self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3617 3618 if stopAll and self.IsInStopOrders(portfolio=overview): 3619 uLogger.debug("Closing all opened stop orders for the instrument with FIGI [{}]. Wait, please...") 3620 self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3621 3622 if self.IsInPortfolio(portfolio=overview): 3623 uLogger.debug("Closing all available (not blocked) opened trade for the instrument with FIGI [{}]. Wait, please...") 3624 self.CloseTrades(instruments=[instrument], portfolio=overview) 3625 3626 @staticmethod 3627 def ParseOrderParameters(operation, **inputParameters): 3628 """ 3629 Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders. 3630 3631 :param operation: string "Buy" or "Sell". 3632 :param inputParameters: this is dict of strings that looks like this 3633 `{"lots": "L_int,...", "prices": "P_float,..."}` where 3634 "lots" key: one or more lot values (integer numbers) to open with every limit-order 3635 "prices" key: one or more prices to open limit-orders 3636 Counts of values in lots and prices lists must be equals! 3637 :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]` 3638 """ 3639 # TODO: update order grid work with api v2 3640 pass 3641 # uLogger.debug("Input parameters: {}".format(inputParameters)) 3642 # 3643 # if operation is None or not operation or operation not in ("Buy", "Sell"): 3644 # uLogger.error("You must define operation type: 'Buy' or 'Sell'!") 3645 # raise Exception("Incorrect value") 3646 # 3647 # if "l" in inputParameters.keys(): 3648 # inputParameters["lots"] = inputParameters.pop("l") 3649 # 3650 # if "p" in inputParameters.keys(): 3651 # inputParameters["prices"] = inputParameters.pop("p") 3652 # 3653 # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys(): 3654 # uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!") 3655 # raise Exception("Incorrect value") 3656 # 3657 # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")] 3658 # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")] 3659 # 3660 # if len(lots) != len(prices): 3661 # uLogger.error("'lots' and 'prices' lists must have equal length of values!") 3662 # raise Exception("Incorrect value") 3663 # 3664 # uLogger.debug("Extracted parameters for orders:") 3665 # uLogger.debug("lots = {}".format(lots)) 3666 # uLogger.debug("prices = {}".format(prices)) 3667 # 3668 # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...] 3669 # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))] 3670 # uLogger.debug("Order parameters: {}".format(result)) 3671 # 3672 # return result 3673 3674 def IsInPortfolio(self, portfolio: dict = None) -> bool: 3675 """ 3676 Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`. 3677 3678 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3679 :return: `True` if portfolio contains open position with given instrument, `False` otherwise. 3680 """ 3681 result = False 3682 msg = "Instrument not defined!" 3683 3684 if portfolio is None or not portfolio: 3685 portfolio = self.Overview(show=False) 3686 3687 if self._ticker: 3688 uLogger.debug("Searching instrument with ticker [{}] throw opened positions list...".format(self._ticker)) 3689 msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker) 3690 3691 for iType in TKS_INSTRUMENTS: 3692 for instrument in portfolio["stat"][iType]: 3693 if instrument["ticker"] == self._ticker: 3694 result = True 3695 msg = "Instrument with ticker [{}] is present in open positions".format(self._ticker) 3696 break 3697 3698 elif self._figi: 3699 uLogger.debug("Searching instrument with FIGI [{}] throw opened positions list...".format(self._figi)) 3700 msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi) 3701 3702 for iType in TKS_INSTRUMENTS: 3703 for instrument in portfolio["stat"][iType]: 3704 if instrument["figi"] == self._figi: 3705 result = True 3706 msg = "Instrument with FIGI [{}] is present in open positions".format(self._figi) 3707 break 3708 3709 else: 3710 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3711 3712 uLogger.debug(msg) 3713 3714 return result 3715 3716 def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict: 3717 """ 3718 Returns instrument from the user's portfolio if it presents there. 3719 Instrument must be defined by `ticker` (highly priority) or `figi`. 3720 3721 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3722 :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise. 3723 """ 3724 result = None 3725 msg = "Instrument not defined!" 3726 3727 if portfolio is None or not portfolio: 3728 portfolio = self.Overview(show=False) 3729 3730 if self._ticker: 3731 uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self._ticker)) 3732 msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker) 3733 3734 for iType in TKS_INSTRUMENTS: 3735 for instrument in portfolio["stat"][iType]: 3736 if instrument["ticker"] == self._ticker: 3737 result = instrument 3738 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self._ticker, instrument["figi"]) 3739 break 3740 3741 elif self._figi: 3742 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self._figi)) 3743 msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi) 3744 3745 for iType in TKS_INSTRUMENTS: 3746 for instrument in portfolio["stat"][iType]: 3747 if instrument["figi"] == self._figi: 3748 result = instrument 3749 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self._figi) 3750 break 3751 3752 else: 3753 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3754 3755 uLogger.debug(msg) 3756 3757 return result 3758 3759 def IsInLimitOrders(self, portfolio: dict = None) -> bool: 3760 """ 3761 Checks if instrument is in the limit orders list. Instrument must be defined by `ticker` (highly priority) or `figi`. 3762 3763 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3764 3765 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3766 :return: `True` if limit orders list contains some limit orders for the instrument, `False` otherwise. 3767 """ 3768 result = False 3769 msg = "Instrument not defined!" 3770 3771 if portfolio is None or not portfolio: 3772 portfolio = self.Overview(show=False) 3773 3774 if self._ticker: 3775 uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker)) 3776 msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker) 3777 3778 for instrument in portfolio["stat"]["orders"]: 3779 if instrument["ticker"] == self._ticker: 3780 result = True 3781 msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker) 3782 break 3783 3784 elif self._figi: 3785 uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi)) 3786 msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi) 3787 3788 for instrument in portfolio["stat"]["orders"]: 3789 if instrument["figi"] == self._figi: 3790 result = True 3791 msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi) 3792 break 3793 3794 else: 3795 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3796 3797 uLogger.debug(msg) 3798 3799 return result 3800 3801 def GetLimitOrderIDs(self, portfolio: dict = None) -> list[str]: 3802 """ 3803 Returns list with all `orderID`s of opened pending limit orders for the instrument. 3804 Instrument must be defined by `ticker` (highly priority) or `figi`. 3805 3806 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3807 3808 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3809 :return: list with `orderID`s of limit orders. 3810 """ 3811 result = [] 3812 msg = "Instrument not defined!" 3813 3814 if portfolio is None or not portfolio: 3815 portfolio = self.Overview(show=False) 3816 3817 if self._ticker: 3818 uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker)) 3819 msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker) 3820 3821 for instrument in portfolio["stat"]["orders"]: 3822 if instrument["ticker"] == self._ticker: 3823 result.append(instrument["orderID"]) 3824 3825 if result: 3826 msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker) 3827 3828 elif self._figi: 3829 uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi)) 3830 msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi) 3831 3832 for instrument in portfolio["stat"]["orders"]: 3833 if instrument["figi"] == self._figi: 3834 result.append(instrument["orderID"]) 3835 3836 if result: 3837 msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi) 3838 3839 else: 3840 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3841 3842 uLogger.debug(msg) 3843 3844 return result 3845 3846 def IsInStopOrders(self, portfolio: dict = None) -> bool: 3847 """ 3848 Checks if instrument is in the stop orders list. Instrument must be defined by `ticker` (highly priority) or `figi`. 3849 3850 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3851 3852 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3853 :return: `True` if stop orders list contains some stop orders for the instrument, `False` otherwise. 3854 """ 3855 result = False 3856 msg = "Instrument not defined!" 3857 3858 if portfolio is None or not portfolio: 3859 portfolio = self.Overview(show=False) 3860 3861 if self._ticker: 3862 uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker)) 3863 msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker) 3864 3865 for instrument in portfolio["stat"]["stopOrders"]: 3866 if instrument["ticker"] == self._ticker: 3867 result = True 3868 msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker) 3869 break 3870 3871 elif self._figi: 3872 uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi)) 3873 msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi) 3874 3875 for instrument in portfolio["stat"]["stopOrders"]: 3876 if instrument["figi"] == self._figi: 3877 result = True 3878 msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi) 3879 break 3880 3881 else: 3882 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3883 3884 uLogger.debug(msg) 3885 3886 return result 3887 3888 def GetStopOrderIDs(self, portfolio: dict = None) -> list[str]: 3889 """ 3890 Returns list with all `orderID`s of opened stop orders for the instrument. 3891 Instrument must be defined by `ticker` (highly priority) or `figi`. 3892 3893 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3894 3895 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3896 :return: list with `orderID`s of stop orders. 3897 """ 3898 result = [] 3899 msg = "Instrument not defined!" 3900 3901 if portfolio is None or not portfolio: 3902 portfolio = self.Overview(show=False) 3903 3904 if self._ticker: 3905 uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker)) 3906 msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker) 3907 3908 for instrument in portfolio["stat"]["stopOrders"]: 3909 if instrument["ticker"] == self._ticker: 3910 result.append(instrument["orderID"]) 3911 3912 if result: 3913 msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker) 3914 3915 elif self._figi: 3916 uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi)) 3917 msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi) 3918 3919 for instrument in portfolio["stat"]["stopOrders"]: 3920 if instrument["figi"] == self._figi: 3921 result.append(instrument["orderID"]) 3922 3923 if result: 3924 msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi) 3925 3926 else: 3927 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3928 3929 uLogger.debug(msg) 3930 3931 return result 3932 3933 def RequestLimits(self) -> dict: 3934 """ 3935 Method for obtaining the available funds for withdrawal for current `accountId`. 3936 3937 See also: 3938 - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits 3939 - `OverviewLimits()` method 3940 3941 :return: dict with raw data from server that contains free funds for withdrawal. Example of dict: 3942 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`. 3943 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency 3944 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures. 3945 """ 3946 if self.accountId is None or not self.accountId: 3947 uLogger.error("Variable `accountId` must be defined for using this method!") 3948 raise Exception("Account ID required") 3949 3950 uLogger.debug("Requesting current available funds for withdrawal. Wait, please...") 3951 3952 self.body = str({"accountId": self.accountId}) 3953 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits" 3954 rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3955 3956 if self.moreDebug: 3957 uLogger.debug("Records about available funds for withdrawal successfully received") 3958 3959 return rawLimits 3960 3961 def OverviewLimits(self, show: bool = False, onlyFiles=False) -> dict: 3962 """ 3963 Method for parsing and show table with available funds for withdrawal for current `accountId`. 3964 3965 See also: `RequestLimits()`. 3966 3967 :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log. 3968 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 3969 :return: dict with raw parsed data from server and some calculated statistics about it. 3970 """ 3971 if self.accountId is None or not self.accountId: 3972 uLogger.error("Variable `accountId` must be defined for using this method!") 3973 raise Exception("Account ID required") 3974 3975 rawLimits = self.RequestLimits() # raw response with current available funds for withdrawal 3976 3977 view = { 3978 "rawLimits": rawLimits, 3979 "limits": { # parsed data for every currency: 3980 "money": { # this is an array of portfolio currency positions 3981 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"] 3982 }, 3983 "blocked": { # this is an array of blocked currency 3984 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"] 3985 }, 3986 "blockedGuarantee": { # this is locked money under collateral for futures 3987 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"] 3988 }, 3989 }, 3990 } 3991 3992 # --- Prepare text table with limits in human-readable format: 3993 if show or onlyFiles: 3994 info = [ 3995 "# Withdrawal limits\n\n", 3996 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 3997 "* **Account ID:** [{}]\n".format(self.accountId), 3998 ] 3999 4000 if view["limits"]["money"]: 4001 info.extend([ 4002 "\n| Currencies | Total | Available for withdrawal | Blocked for trade | Futures guarantee |\n", 4003 "|------------|---------------|--------------------------|-------------------|-------------------|\n", 4004 ]) 4005 4006 else: 4007 info.append("\nNo withdrawal limits\n") 4008 4009 for curr in view["limits"]["money"].keys(): 4010 blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0 4011 blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0 4012 availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee) 4013 4014 infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format( 4015 "[{}]".format(curr), 4016 "{:.2f}".format(view["limits"]["money"][curr]), 4017 "{:.2f}".format(availableMoney), 4018 "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—", 4019 "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—", 4020 ) 4021 4022 if curr == "rub": 4023 info.insert(5, infoStr) # hack: insert "rub" at the first position in table and after headers 4024 4025 else: 4026 info.append(infoStr) 4027 4028 infoText = "".join(info) 4029 4030 if show and not onlyFiles: 4031 uLogger.info(infoText) 4032 4033 if self.withdrawalLimitsFile and (show or onlyFiles): 4034 with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH: 4035 fH.write(infoText) 4036 4037 uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile))) 4038 4039 if self.useHTMLReports: 4040 htmlFilePath = self.withdrawalLimitsFile.replace(".md", ".html") if self.withdrawalLimitsFile.endswith(".md") else self.withdrawalLimitsFile + ".html" 4041 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4042 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Withdrawal limits", commonCSS=COMMON_CSS, markdown=infoText)) 4043 4044 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4045 4046 return view 4047 4048 def RequestAccounts(self) -> dict: 4049 """ 4050 Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`. 4051 4052 See also: 4053 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts 4054 - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account 4055 - `OverviewUserInfo()` method 4056 4057 :return: dict with raw data from server that contains accounts info. Example of dict: 4058 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", 4059 "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", 4060 "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`. 4061 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now. 4062 """ 4063 uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...") 4064 4065 self.body = str({}) 4066 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts" 4067 rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST") 4068 4069 if self.moreDebug: 4070 uLogger.debug("Records about available accounts successfully received") 4071 4072 return rawAccounts 4073 4074 def RequestUserInfo(self) -> dict: 4075 """ 4076 Method for requesting common user's information. 4077 4078 See also: 4079 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo 4080 - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest 4081 - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with 4082 - `OverviewUserInfo()` method 4083 4084 :return: dict with raw data from server that contains user's information. Example of dict: 4085 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", 4086 "russian_shares", "structured_income_bonds"], "tariff": "premium"}`. 4087 """ 4088 uLogger.debug("Requesting common user's information. Wait, please...") 4089 4090 self.body = str({}) 4091 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo" 4092 rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST") 4093 4094 if self.moreDebug: 4095 uLogger.debug("Records about current user successfully received") 4096 4097 return rawUserInfo 4098 4099 def RequestMarginStatus(self, accountId: str = None) -> dict: 4100 """ 4101 Method for requesting margin calculation for defined account ID. 4102 4103 See also: 4104 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes 4105 - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse 4106 - `OverviewUserInfo()` method 4107 4108 :param accountId: string with numeric account ID. If `None`, then used class field `accountId`. 4109 :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. 4110 Example of responses: 4111 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`. 4112 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, 4113 "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, 4114 "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, 4115 "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, 4116 "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`. 4117 """ 4118 if accountId is None or not accountId: 4119 if self.accountId is None or not self.accountId: 4120 uLogger.error("Variable `accountId` must be defined for using this method!") 4121 raise Exception("Account ID required") 4122 4123 else: 4124 accountId = self.accountId # use `self.accountId` (main ID) by default 4125 4126 uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId)) 4127 4128 self.body = str({"accountId": accountId}) 4129 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes" 4130 rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST") 4131 4132 if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}: 4133 uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId)) 4134 rawMargin = {} 4135 4136 else: 4137 if self.moreDebug: 4138 uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId)) 4139 4140 return rawMargin 4141 4142 def RequestTariffLimits(self) -> dict: 4143 """ 4144 Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`. 4145 4146 See also: 4147 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff 4148 - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest 4149 - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit 4150 - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit 4151 - `OverviewUserInfo()` method 4152 4153 :return: dict with raw data from server that contains limits of current tariff. Example of dict: 4154 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], 4155 "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`. 4156 """ 4157 uLogger.debug("Requesting limits of current tariff. Wait, please...") 4158 4159 self.body = str({}) 4160 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff" 4161 rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 4162 4163 if self.moreDebug: 4164 uLogger.debug("Records with limits of current tariff successfully received") 4165 4166 return rawTariffLimits 4167 4168 def RequestBondCoupons(self, iJSON: dict) -> dict: 4169 """ 4170 Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown 4171 then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`. 4172 All dates are in UTC timezone. 4173 4174 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons 4175 Documentation: 4176 - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest 4177 - response: https://tinkoff.github.io/investAPI/instruments/#coupon 4178 4179 See also: `ExtendBondsData()`. 4180 4181 :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self._ticker]` 4182 If raw iJSON is not data of bond then server returns an error [400] with message: 4183 `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`. 4184 :return: dictionary with bond payment calendar. Response example 4185 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", 4186 "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, 4187 "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", 4188 "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}` 4189 """ 4190 if iJSON["figi"] is None or not iJSON["figi"]: 4191 uLogger.error("FIGI must be defined for using this method!") 4192 raise Exception("FIGI required") 4193 4194 startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z" 4195 endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z" 4196 4197 uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format( 4198 "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "", 4199 self._figi, 4200 startDate, 4201 endDate, 4202 )) 4203 4204 self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate}) 4205 calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons" 4206 calendar = self.SendAPIRequest(calendarURL, reqType="POST") 4207 4208 if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}: 4209 uLogger.warning("Instrument type is not bond!") 4210 4211 else: 4212 if self.moreDebug: 4213 uLogger.debug("Records about bond payment calendar successfully received") 4214 4215 return calendar 4216 4217 def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame: 4218 """ 4219 Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider 4220 Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, 4221 coupon yields, current yields and some statistics etc. 4222 4223 WARNING! This is too long operation if a lot of bonds requested from broker server. 4224 4225 See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`. 4226 4227 :param instruments: list of strings with tickers or FIGIs. 4228 :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`, 4229 for further used by data scientists or stock analytics. 4230 :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. 4231 In XLSX-file and Pandas DataFrame fields mean: 4232 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond 4233 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon 4234 """ 4235 if instruments is None or not instruments: 4236 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 4237 raise Exception("Ticker or FIGI required") 4238 4239 if isinstance(instruments, str): 4240 instruments = [instruments] 4241 4242 uniqueInstruments = self.GetUniqueFIGIs(instruments) 4243 4244 uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...") 4245 4246 iCount = len(uniqueInstruments) 4247 tooLong = iCount >= 20 4248 if tooLong: 4249 uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...") 4250 4251 bonds = None 4252 for i, self._figi in enumerate(uniqueInstruments): 4253 instrument = self.SearchByFIGI(requestPrice=False) # raw data about instrument from server 4254 4255 if "type" in instrument.keys() and instrument["type"] == "Bonds": 4256 # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond 4257 rawBond = self.SearchByFIGI(requestPrice=True) 4258 4259 # Widen raw data with UTC current time (iData["actualDateTime"]): 4260 actualDate = datetime.now(tzutc()) 4261 iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond 4262 4263 # Widen raw data with bond payment calendar (iData["rawCalendar"]): 4264 iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)} 4265 4266 # Replace some values with human-readable: 4267 iData["nominalCurrency"] = iData["nominal"]["currency"] 4268 iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"]) 4269 iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"]) 4270 iData["aciCurrency"] = iData["aciValue"]["currency"] 4271 iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"]) 4272 iData["issueSize"] = int(iData["issueSize"]) 4273 iData["issueSizePlan"] = int(iData["issueSizePlan"]) 4274 iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]] 4275 iData["step"] = iData["step"] if "step" in iData.keys() else 0 4276 iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]] 4277 iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0 4278 iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0 4279 iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0 4280 iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0 4281 iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0 4282 iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0 4283 4284 # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date): 4285 iData["limitUpPercent"] = iData["currentPrice"]["limitUp"] # max price on current day in percents of nominal 4286 iData["limitDownPercent"] = iData["currentPrice"]["limitDown"] # min price on current day in percents of nominal 4287 iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"] # last price on market in percents of nominal 4288 iData["closePricePercent"] = iData["currentPrice"]["closePrice"] # previous day close in percents of nominal 4289 iData["changes"] = iData["currentPrice"]["changes"] # this is percent of changes between `currentPrice` and `lastPrice` 4290 iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100 # max price on current day is `limitUpPercent` * `nominal` 4291 iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100 # min price on current day is `limitDownPercent` * `nominal` 4292 iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100 # last price on market is `lastPricePercent` * `nominal` 4293 iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100 # previous day close is `closePricePercent` * `nominal` 4294 iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"] # this is delta between last deal price and last close 4295 4296 # Widen raw data with calendar data from `rawCalendar` values: 4297 calendarData = [] 4298 if "events" in iData["rawCalendar"].keys(): 4299 for item in iData["rawCalendar"]["events"]: 4300 calendarData.append({ 4301 "couponDate": item["couponDate"], 4302 "couponNumber": int(item["couponNumber"]), 4303 "fixDate": item["fixDate"] if "fixDate" in item.keys() else "", 4304 "payCurrency": item["payOneBond"]["currency"], 4305 "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]), 4306 "couponType": TKS_COUPON_TYPES[item["couponType"]], 4307 "couponStartDate": item["couponStartDate"], 4308 "couponEndDate": item["couponEndDate"], 4309 "couponPeriod": item["couponPeriod"], 4310 }) 4311 4312 # if maturity date is unknown then uses the latest date in bond payment calendar for it: 4313 if "maturityDate" not in iData.keys(): 4314 iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else "" 4315 4316 # Widen raw data with Coupon Rate. 4317 # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%: 4318 iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData]) 4319 iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData]) 4320 iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0. 4321 4322 # Widen raw data with Yield to Maturity (YTM) on current date. 4323 # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%: 4324 maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None 4325 iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None 4326 iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate]) 4327 iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"] # sum of all last coupons minus current ACI value 4328 iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0. 4329 4330 iData["calendar"] = calendarData # adds calendar at the end 4331 4332 # Remove not used data: 4333 iData.pop("uid") 4334 iData.pop("positionUid") 4335 iData.pop("currentPrice") 4336 iData.pop("rawCalendar") 4337 4338 colNames = list(iData.keys()) 4339 if bonds is None: 4340 bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames)) 4341 4342 else: 4343 bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True) 4344 4345 else: 4346 uLogger.warning("Instrument is not a bond!") 4347 4348 processed = round(100 * (i + 1) / iCount, 1) 4349 if tooLong and processed % 5 == 0: 4350 uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount)) 4351 4352 else: 4353 uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount)) 4354 4355 bonds.index = bonds["ticker"].tolist() # replace indexes with ticker names 4356 4357 # Saving bonds from Pandas DataFrame to XLSX sheet: 4358 if xlsx and self.bondsXLSXFile: 4359 with pd.ExcelWriter( 4360 path=self.bondsXLSXFile, 4361 date_format=TKS_DATE_FORMAT, 4362 datetime_format=TKS_DATE_TIME_FORMAT, 4363 mode="w", 4364 ) as writer: 4365 bonds.to_excel( 4366 writer, 4367 sheet_name="Extended bonds data", 4368 index=True, 4369 encoding="UTF-8", 4370 freeze_panes=(1, 1), 4371 ) # saving as XLSX-file with freeze first row and column as headers 4372 4373 uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile))) 4374 4375 return bonds 4376 4377 def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame: 4378 """ 4379 Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default. 4380 4381 WARNING! This is too long operation if a lot of bonds requested from broker server. 4382 4383 See also: `ShowBondsCalendar()`, `ExtendBondsData()`. 4384 4385 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4386 extended information about bonds: main info, current prices, bond payment calendar, 4387 coupon yields, current yields and some statistics etc. 4388 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4389 :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default, 4390 for further used by data scientists or stock analytics. 4391 :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon 4392 """ 4393 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4394 extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False) 4395 4396 uLogger.debug("Generating bond payments calendar data. Wait, please...") 4397 4398 colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"] 4399 colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"] 4400 calendar = None 4401 for bond in extBonds.iterrows(): 4402 for item in bond[1]["calendar"]: 4403 cData = { 4404 "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()), 4405 "couponDate": item["couponDate"], 4406 "figi": bond[1]["figi"], 4407 "ticker": bond[1]["ticker"], 4408 "name": bond[1]["name"], 4409 "couponNumber": item["couponNumber"], 4410 "payOneBond": item["payOneBond"], 4411 "payCurrency": item["payCurrency"], 4412 "couponType": item["couponType"], 4413 "couponPeriod": item["couponPeriod"], 4414 "fixDate": item["fixDate"], 4415 "couponStartDate": item["couponStartDate"], 4416 "couponEndDate": item["couponEndDate"], 4417 } 4418 4419 if calendar is None: 4420 calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID)) 4421 4422 else: 4423 calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True) 4424 4425 if calendar is not None: 4426 calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True) # sort all payments for all bonds by payment date 4427 4428 # Saving calendar from Pandas DataFrame to XLSX sheet: 4429 if xlsx: 4430 xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx" 4431 4432 with pd.ExcelWriter( 4433 path=xlsxCalendarFile, 4434 date_format=TKS_DATE_FORMAT, 4435 datetime_format=TKS_DATE_TIME_FORMAT, 4436 mode="w", 4437 ) as writer: 4438 humanReadable = calendar.copy(deep=True) 4439 humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0]) 4440 humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0]) 4441 humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0]) 4442 humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0]) 4443 humanReadable.columns = colNames # human-readable column names 4444 4445 humanReadable.to_excel( 4446 writer, 4447 sheet_name="Bond payments calendar", 4448 index=False, 4449 encoding="UTF-8", 4450 freeze_panes=(1, 2), 4451 ) # saving as XLSX-file with freeze first row and column as headers 4452 4453 del humanReadable # release df in memory 4454 4455 uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile))) 4456 4457 return calendar 4458 4459 def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True, onlyFiles=False) -> str: 4460 """ 4461 Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond. 4462 Also, creates Markdown file with calendar data, `calendar.md` by default. 4463 4464 See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`. 4465 4466 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4467 extended information about bonds: main info, current prices, bond payment calendar, 4468 coupon yields, current yields and some statistics etc. 4469 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4470 :param show: if `True` then also printing bonds payment calendar to the console, 4471 otherwise save to file `calendarFile` only. `False` by default. 4472 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 4473 :return: multilines text in Markdown format with bonds payment calendar as a table. 4474 """ 4475 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4476 extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=show or onlyFiles) 4477 4478 infoText = "# Bond payments calendar\n\n" 4479 4480 calendar = self.CreateBondsCalendar(extBonds, xlsx=show or onlyFiles) # generate Pandas DataFrame with full calendar data 4481 4482 if not (calendar is None or calendar.empty): 4483 splitLine = "| | | | | | | | | |\n" 4484 4485 info = [ 4486 "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4487 "| Paid | Payment date | FIGI | Ticker | No. | Value | Type | Period | End registry date |\n", 4488 "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n", 4489 ] 4490 4491 newMonth = False 4492 notOneBond = calendar["figi"].nunique() > 1 4493 for i, bond in enumerate(calendar.iterrows()): 4494 if newMonth and notOneBond: 4495 info.append(splitLine) 4496 4497 info.append( 4498 "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format( 4499 " √" if bond[1]["paid"] else " —", 4500 bond[1]["couponDate"].split("T")[0], 4501 bond[1]["figi"], 4502 bond[1]["ticker"], 4503 bond[1]["couponNumber"], 4504 "{} {}".format( 4505 "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."), 4506 bond[1]["payCurrency"], 4507 ), 4508 bond[1]["couponType"], 4509 bond[1]["couponPeriod"], 4510 bond[1]["fixDate"].split("T")[0], 4511 ) 4512 ) 4513 4514 if i < len(calendar.values) - 1: 4515 curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4516 nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4517 newMonth = False if curDate.month == nextDate.month else True 4518 4519 else: 4520 newMonth = False 4521 4522 infoText += "".join(info) 4523 4524 if show and not onlyFiles: 4525 uLogger.info("{}".format(infoText)) 4526 4527 if self.calendarFile is not None and (show or onlyFiles): 4528 with open(self.calendarFile, "w", encoding="UTF-8") as fH: 4529 fH.write(infoText) 4530 4531 uLogger.info("Bond payments calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile))) 4532 4533 if self.useHTMLReports: 4534 htmlFilePath = self.calendarFile.replace(".md", ".html") if self.calendarFile.endswith(".md") else self.calendarFile + ".html" 4535 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4536 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Bond payments calendar", commonCSS=COMMON_CSS, markdown=infoText)) 4537 4538 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4539 4540 else: 4541 infoText += "No data\n" 4542 4543 return infoText 4544 4545 def OverviewAccounts(self, show: bool = False, onlyFiles=False) -> dict: 4546 """ 4547 Method for parsing and show simple table with all available user accounts. 4548 4549 See also: `RequestAccounts()` and `OverviewUserInfo()` methods. 4550 4551 :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log. 4552 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 4553 :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict: 4554 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, 4555 "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", 4556 "status": "Opened and active account", "opened": "2018-05-23 00:00:00", 4557 "closed": "—", "access": "Full access" }, ...}}` 4558 """ 4559 rawAccounts = self.RequestAccounts() # Raw responses with accounts 4560 4561 # This is an array of dict with user accounts, its `accountId`s and some parsed data: 4562 accounts = { 4563 item["id"]: { 4564 "type": TKS_ACCOUNT_TYPES[item["type"]], 4565 "name": item["name"], 4566 "status": TKS_ACCOUNT_STATUSES[item["status"]], 4567 "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4568 "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—", 4569 "access": TKS_ACCESS_LEVELS[item["accessLevel"]], 4570 } for item in rawAccounts["accounts"] 4571 } 4572 4573 # Raw and parsed data with some fields replaced in "stat" section: 4574 view = { 4575 "rawAccounts": rawAccounts, 4576 "stat": accounts, 4577 } 4578 4579 # --- Prepare simple text table with only accounts data in human-readable format: 4580 if show or onlyFiles: 4581 info = [ 4582 "# User accounts\n\n", 4583 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4584 "| Account ID | Type | Status | Name |\n", 4585 "|--------------|---------------------------|---------------------------|--------------------------------|\n", 4586 ] 4587 4588 for account in view["stat"].keys(): 4589 info.extend([ 4590 "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format( 4591 account, 4592 view["stat"][account]["type"], 4593 view["stat"][account]["status"], 4594 view["stat"][account]["name"], 4595 ) 4596 ]) 4597 4598 infoText = "".join(info) 4599 4600 if show and not onlyFiles: 4601 uLogger.info(infoText) 4602 4603 if self.userAccountsFile and (show or onlyFiles): 4604 with open(self.userAccountsFile, "w", encoding="UTF-8") as fH: 4605 fH.write(infoText) 4606 4607 uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile))) 4608 4609 if self.useHTMLReports: 4610 htmlFilePath = self.userAccountsFile.replace(".md", ".html") if self.userAccountsFile.endswith(".md") else self.userAccountsFile + ".html" 4611 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4612 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User accounts", commonCSS=COMMON_CSS, markdown=infoText)) 4613 4614 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4615 4616 return view 4617 4618 def OverviewUserInfo(self, show: bool = False, onlyFiles=False) -> dict: 4619 """ 4620 Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). 4621 4622 See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods. 4623 4624 :param show: if `False` then only dictionary returns, if `True` then also print user's data to log. 4625 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 4626 :return: dict with raw parsed data from server and some calculated statistics about it. 4627 """ 4628 overview = self.Overview(show=False) # Request current user portfolio for the ability to calculate missing funds 4629 tmpTicker = self._ticker 4630 self._ticker = "RUB000UTSTOM" # This instrument show in rub how much money cost current margin 4631 missing = self.GetInstrumentFromPortfolio(portfolio=overview) 4632 self._ticker = tmpTicker 4633 4634 rawUserInfo = self.RequestUserInfo() # Raw response with common user info 4635 overviewAccount = self.OverviewAccounts(show=False) # Raw and parsed accounts data 4636 rawAccounts = overviewAccount["rawAccounts"] # Raw response with user accounts data 4637 accounts = overviewAccount["stat"] # Dict with only statistics about user accounts 4638 rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()} # Raw response with margin calculation for every account ID 4639 rawTariffLimits = self.RequestTariffLimits() # Raw response with limits of current tariff 4640 4641 # This is dict with parsed common user data: 4642 userInfo = { 4643 "premium": "Yes" if rawUserInfo["premStatus"] else "No", 4644 "qualified": "Yes" if rawUserInfo["qualStatus"] else "No", 4645 "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]], 4646 "tariff": rawUserInfo["tariff"], 4647 } 4648 4649 # This is an array of dict with parsed margin statuses for every account IDs: 4650 margins = {} 4651 for accountId in accounts.keys(): 4652 if rawMargins[accountId]: 4653 margins[accountId] = { 4654 "currency": rawMargins[accountId]["liquidPortfolio"]["currency"], 4655 "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]), 4656 "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]), 4657 "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]), 4658 "diff": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]), 4659 "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]), 4660 "missing": missing["volume"], 4661 } 4662 4663 else: 4664 margins[accountId] = {} # Server response: margin status is disabled for current accountId 4665 4666 unary = {} # unary-connection limits 4667 for item in rawTariffLimits["unaryLimits"]: 4668 if item["limitPerMinute"] in unary.keys(): 4669 unary[item["limitPerMinute"]].extend(item["methods"]) 4670 4671 else: 4672 unary[item["limitPerMinute"]] = item["methods"] 4673 4674 stream = {} # stream-connection limits 4675 for item in rawTariffLimits["streamLimits"]: 4676 if item["limit"] in stream.keys(): 4677 stream[item["limit"]].extend(item["streams"]) 4678 4679 else: 4680 stream[item["limit"]] = item["streams"] 4681 4682 # This is dict with parsed limits of current tariff (connections, API methods etc.): 4683 limits = { 4684 "unary": unary, 4685 "stream": stream, 4686 } 4687 4688 # Raw and parsed data as an output result: 4689 view = { 4690 "rawUserInfo": rawUserInfo, 4691 "rawAccounts": rawAccounts, 4692 "rawMargins": rawMargins, 4693 "rawTariffLimits": rawTariffLimits, 4694 "stat": { 4695 "overview": overview, 4696 "userInfo": userInfo, 4697 "accounts": accounts, 4698 "margins": margins, 4699 "limits": limits, 4700 }, 4701 } 4702 4703 # --- Prepare text table with user information in human-readable format: 4704 if show or onlyFiles: 4705 info = [ 4706 "# Full user information\n\n", 4707 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4708 "## Common information\n\n", 4709 "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]), 4710 "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]), 4711 "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]), 4712 "* **Allowed to work with instruments:**\n{}\n".format("".join([" - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])), 4713 "\n## User accounts\n\n", 4714 ] 4715 4716 for account in view["stat"]["accounts"].keys(): 4717 info.extend([ 4718 "### ID: [{}]\n\n".format(account), 4719 "| Parameters | Values |\n", 4720 "|----------------------|--------------------------------------------------------------|\n", 4721 "| Account type: | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]), 4722 "| Account name: | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]), 4723 "| Account status: | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]), 4724 "| Access level: | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]), 4725 "| Date opened: | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]), 4726 "| Date closed: | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]), 4727 ]) 4728 4729 if margins[account]: 4730 info.extend([ 4731 "| Margin status: | Enabled |\n", 4732 "| - Liquid portfolio: | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])), 4733 "| - Margin starting: | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])), 4734 "| - Margin minimum: | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])), 4735 "| - Margin difference: | {:<60} |\n".format("{} {}".format(margins[account]["diff"], margins[account]["currency"])), 4736 "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)), 4737 "| - Not covered funds: | {:<60} |\n\n".format("{:.2f} {}".format(margins[account]["missing"], margins[account]["currency"])), 4738 ]) 4739 4740 else: 4741 info.append("| Margin status: | Disabled |\n\n") 4742 4743 info.extend([ 4744 "\n## Current user tariff limits\n", 4745 "\n### See also\n", 4746 "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n", 4747 "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n", 4748 " - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n", 4749 " - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n", 4750 "\n### Unary limits\n", 4751 ]) 4752 4753 if unary: 4754 for key, values in sorted(unary.items()): 4755 info.append("\n* Max requests per minute: {}\n".format(key)) 4756 4757 for value in values: 4758 info.append(" - {}\n".format(value)) 4759 4760 else: 4761 info.append("\nNot available\n") 4762 4763 info.append("\n### Stream limits\n") 4764 4765 if stream: 4766 for key, values in sorted(stream.items()): 4767 info.append("\n* Max stream connections: {}\n".format(key)) 4768 4769 for value in values: 4770 info.append(" - {}\n".format(value)) 4771 4772 else: 4773 info.append("\nNot available\n") 4774 4775 infoText = "".join(info) 4776 4777 if show and not onlyFiles: 4778 uLogger.info(infoText) 4779 4780 if self.userInfoFile and (show or onlyFiles): 4781 with open(self.userInfoFile, "w", encoding="UTF-8") as fH: 4782 fH.write(infoText) 4783 4784 uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile))) 4785 4786 if self.useHTMLReports: 4787 htmlFilePath = self.userInfoFile.replace(".md", ".html") if self.userInfoFile.endswith(".md") else self.userInfoFile + ".html" 4788 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4789 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User info", commonCSS=COMMON_CSS, markdown=infoText)) 4790 4791 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4792 4793 return view
This class implements methods to work with Tinkoff broker server.
Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/
About token: https://tinkoff.github.io/investAPI/token/
88 def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None: 89 """ 90 Main class init. 91 92 :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`. 93 :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. 94 Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`. 95 :param useCache: use default cache file with raw data to use instead of `iList`. 96 True by default. Cache is auto-update if new day has come. 97 If you don't want to use cache and always updates raw data then set `useCache=False`. 98 :param defaultCache: path to default cache file. `dump.json` by default. 99 """ 100 if token is None or not token: 101 try: 102 self.token = r"{}".format(os.environ["TKS_API_TOKEN"]) 103 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/") 104 105 except KeyError: 106 uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/") 107 raise Exception("Token required") 108 109 else: 110 self.token = token # highly priority than environment variable 'TKS_API_TOKEN' 111 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`") 112 113 if accountId is None or not accountId: 114 try: 115 self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"]) 116 uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId)) 117 118 except KeyError: 119 uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).") 120 121 else: 122 self.accountId = accountId # highly priority than environment variable 'TKS_ACCOUNT_ID' 123 uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId)) 124 125 self.version = __version__ # duplicate here used TKSBrokerAPI main version 126 """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only. 127 128 Latest version: https://pypi.org/project/tksbrokerapi/ 129 """ 130 131 self._tag = "" 132 """Identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode. Default: `""` (empty string).""" 133 134 self.__lock = Lock() # initialize multiprocessing mutex lock 135 136 self._precision = 4 # precision, signs after comma, e.g. 2 for instruments like PLZL, 4 for instruments like USDRUB, if -1 then auto detect it when load data-file 137 138 self.aliases = TKS_TICKER_ALIASES 139 """Some aliases instead official tickers. 140 141 See also: `TKSEnums.TKS_TICKER_ALIASES` 142 """ 143 144 self.aliasesKeys = self.aliases.keys() # re-calc only first time at class init 145 146 self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there 147 148 self._ticker = "" 149 """String with ticker, e.g. `GOOGL`. Tickers may be upper case only. 150 151 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 152 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 153 154 See also: `SearchByTicker()`, `SearchInstruments()`. 155 """ 156 157 self._figi = "" 158 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 159 160 See also: `SearchByFIGI()`, `SearchInstruments()`. 161 """ 162 163 self.depth = 1 164 """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI. 165 166 See also: `GetCurrentPrices()`. 167 """ 168 169 self.server = r"https://invest-public-api.tinkoff.ru/rest" 170 """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest 171 172 See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`. 173 """ 174 175 uLogger.debug("Broker API server: {}".format(self.server)) 176 177 self.timeout = 15 178 """Server operations timeout in seconds. Default: `15`. 179 180 See also: `SendAPIRequest()`. 181 """ 182 183 self.headers = { 184 "Content-Type": "application/json", 185 "accept": "application/json", 186 "Authorization": "Bearer {}".format(self.token), 187 "x-app-name": "Tim55667757.TKSBrokerAPI", 188 } 189 """ 190 Headers which send in every request to broker server. Please, do not change it! 191 Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}", "x-app-name": "Tim55667757.TKSBrokerAPI"}`. 192 193 See also: `SendAPIRequest()`. 194 """ 195 196 self.body = None 197 """Request body which send to broker server. Default: `None`. 198 199 See also: `SendAPIRequest()`. 200 """ 201 202 self.moreDebug = False 203 """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default.""" 204 205 self.useHTMLReports = False 206 """ 207 If `True` then TKSBrokerAPI generate also HTML reports from Markdown. `False` by default. 208 209 See also: Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance. 210 """ 211 212 self.historyFile = None 213 """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame. 214 215 See also: `History()`. 216 """ 217 218 self.htmlHistoryFile = "index.html" 219 """Full path to the html file where rendered candles chart stored. Default: `index.html`. 220 221 See also: `ShowHistoryChart()`. 222 """ 223 224 self.instrumentsFile = "instruments.md" 225 """Filename where full available to user instruments list will be saved. Default: `instruments.md`. 226 227 See also: `ShowInstrumentsInfo()`. 228 """ 229 230 self.searchResultsFile = "search-results.md" 231 """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`. 232 233 See also: `SearchInstruments()`. 234 """ 235 236 self.pricesFile = "prices.md" 237 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 238 239 See also: `GetListOfPrices()`. 240 """ 241 242 self.infoFile = "info.md" 243 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 244 245 See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`. 246 """ 247 248 self.bondsXLSXFile = "ext-bonds.xlsx" 249 """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 250 bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`. 251 252 See also: `ExtendBondsData()`. 253 """ 254 255 self.calendarFile = "calendar.md" 256 """Filename where bonds payment calendar will be saved. Default: `calendar.md`. 257 258 Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`. 259 260 See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`. 261 """ 262 263 self.overviewFile = "overview.md" 264 """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`. 265 266 See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`. 267 """ 268 269 self.overviewDigestFile = "overview-digest.md" 270 """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`. 271 272 See also: `Overview()` with parameter `details="digest"`. 273 """ 274 275 self.overviewPositionsFile = "overview-positions.md" 276 """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`. 277 278 See also: `Overview()` with parameter `details="positions"`. 279 """ 280 281 self.overviewOrdersFile = "overview-orders.md" 282 """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`. 283 284 See also: `Overview()` with parameter `details="orders"`. 285 """ 286 287 self.overviewAnalyticsFile = "overview-analytics.md" 288 """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`. 289 290 See also: `Overview()` with parameter `details="analytics"`. 291 """ 292 293 self.overviewBondsCalendarFile = "overview-calendar.md" 294 """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`. 295 296 See also: `Overview()` with parameter `details="calendar"`. 297 """ 298 299 self.reportFile = "deals.md" 300 """Filename where history of deals and trade statistics will be saved. Default: `deals.md`. 301 302 See also: `Deals()`. 303 """ 304 305 self.withdrawalLimitsFile = "limits.md" 306 """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`. 307 308 See also: `OverviewLimits()` and `RequestLimits()`. 309 """ 310 311 self.userInfoFile = "user-info.md" 312 """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`. 313 314 See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`. 315 """ 316 317 self.userAccountsFile = "accounts.md" 318 """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`. 319 320 See also: `OverviewAccounts()`, `RequestAccounts()`. 321 """ 322 323 self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache 324 """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`. 325 326 Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`. 327 328 See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`. 329 """ 330 331 self.iList = None # init iList for raw instruments data 332 """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`. 333 334 See also: `Listing()`, `DumpInstruments()`. 335 """ 336 337 # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server: 338 if useCache: 339 if os.path.exists(self.iListDumpFile): 340 dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc()) # dump modification date and time 341 curTime = datetime.now(tzutc()) 342 343 if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year): 344 uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 345 346 self.DumpInstruments(forceUpdate=True) # updating self.iList and dump file 347 348 else: 349 self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8")) # load iList from dump 350 351 uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format( 352 os.path.abspath(self.iListDumpFile), 353 dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT), 354 )) 355 356 else: 357 uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...") 358 self.DumpInstruments(forceUpdate=True) # updating self.iList and creating default dump file 359 360 else: 361 self.iList = self.Listing() # request new raw instruments data from broker server 362 self.DumpInstruments(forceUpdate=False) # save raw instrument's data to default dump file `iListDumpFile` 363 364 self.priceModel = PriceGenerator() # init PriceGenerator object to work with candles data 365 """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on. 366 367 See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator 368 """
Main class init.
Parameters
- token: Bearer token for Tinkoff Invest API. It can be set from environment variable
TKS_API_TOKEN. - accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
Also, this variable can be set from environment variable
TKS_ACCOUNT_ID. - useCache: use default cache file with raw data to use instead of
iList. True by default. Cache is auto-update if new day has come. If you don't want to use cache and always updates raw data then setuseCache=False. - defaultCache: path to default cache file.
dump.jsonby default.
Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
Latest version: https://pypi.org/project/tksbrokerapi/
Depth of Market (DOM) can be >= 1. Default: 1. It used with --price key to showing DOM with current prices for givens ticker or FIGI.
See also: GetCurrentPrices().
Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and SendAPIRequest().
Headers which send in every request to broker server. Please, do not change it!
Default: {"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}", "x-app-name": "Tim55667757.TKSBrokerAPI"}.
See also: SendAPIRequest().
Enables more debug information in this class, such as net request and response headers in all methods. False by default.
If True then TKSBrokerAPI generate also HTML reports from Markdown. False by default.
See also: Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance.
Full path to the output file where history candles will be saved or updated. Default: None, it mean that returns only Pandas DataFrame.
See also: History().
Full path to the html file where rendered candles chart stored. Default: index.html.
See also: ShowHistoryChart().
Filename where full available to user instruments list will be saved. Default: instruments.md.
See also: ShowInstrumentsInfo().
Filename with all found instruments searched by part of its ticker, FIGI or name. Default: search-results.md.
See also: SearchInstruments().
Filename where prices of selected instruments will be saved. Default: prices.md.
See also: GetListOfPrices().
Filename where prices of selected instruments will be saved. Default: prices.md.
See also: ShowInstrumentsInfo(), RequestBondCoupons() and RequestTradingStatus().
Filename where wider Pandas DataFrame with more information about bonds: main info, current prices,
bonds payment calendar, some statistics will be stored. Default: ext-bonds.xlsx.
See also: ExtendBondsData().
Filename where bonds payment calendar will be saved. Default: calendar.md.
Pandas dataframe with only bonds payment calendar also will be stored to default file calendar.xlsx.
See also: CreateBondsCalendar(), ShowBondsCalendar(), ShowInstrumentInfo(), RequestBondCoupons() and ExtendBondsData().
Filename where current portfolio, open trades and orders will be saved. Default: overview.md.
See also: Overview(), RequestPortfolio(), RequestPositions(), RequestPendingOrders() and RequestStopOrders().
Filename where short digest of the portfolio status will be saved. Default: overview-digest.md.
See also: Overview() with parameter details="digest".
Filename where only open positions, without everything else will be saved. Default: overview-positions.md.
See also: Overview() with parameter details="positions".
Filename where open limits and stop orders will be saved. Default: overview-orders.md.
See also: Overview() with parameter details="orders".
Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: overview-analytics.md.
See also: Overview() with parameter details="analytics".
Filename where only the bonds calendar section will be saved. Default: overview-calendar.md.
See also: Overview() with parameter details="calendar".
Filename where history of deals and trade statistics will be saved. Default: deals.md.
See also: Deals().
Filename where table of funds available for withdrawal will be saved. Default: limits.md.
See also: OverviewLimits() and RequestLimits().
Filename where all available user's data (accountIds, common user information, margin status and tariff connections limit) will be saved. Default: user-info.md.
See also: OverviewUserInfo(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits().
Filename where simple table with all available user accounts (accountIds) will be saved. Default: accounts.md.
See also: OverviewAccounts(), RequestAccounts().
Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: dump.json.
Pandas dataframe with raw instruments data also will be stored to default file dump.xlsx.
See also: DumpInstruments() and DumpInstrumentsAsXLSX().
Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the iListDumpFile.
See also: Listing(), DumpInstruments().
PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
See also: LoadHistory(), ShowHistoryChart() and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
Setter for Identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode. Default: "" (empty string).
Setter for string with ticker, e.g. GOOGL. Tickers may be upper case only.
Use alias for USD000UTSTOM simple as USD, EUR_RUB__TOM as EUR etc.
More tickers aliases here: TKSEnums.TKS_TICKER_ALIASES.
See also: SearchByTicker(), SearchInstruments().
Setter for string with FIGI, e.g. ticker GOOGL has FIGI BBG009S39JX6. FIGIs may be upper case only.
See also: SearchByFIGI(), SearchInstruments().
462 def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict: 463 """ 464 Send GET or POST request to broker server and receive JSON object. 465 466 self.header: must be defining with dictionary of headers. 467 self.body: if define then used as request body. None by default. 468 self.timeout: global request timeout, 15 seconds by default. 469 :param url: url with REST request. 470 :param reqType: send "GET" or "POST" request. "GET" by default. 471 :param retry: how many times retry after first request if an 5xx server errors occurred. 472 :param pause: sleep time in seconds between retries. 473 :return: response JSON (dictionary) from broker. 474 """ 475 if reqType.upper() not in ("GET", "POST"): 476 uLogger.error("You can define request type: `GET` or `POST`!") 477 raise Exception("Incorrect value") 478 479 if self.moreDebug: 480 uLogger.debug("Request parameters:") 481 uLogger.debug(" - REST API URL: {}".format(url)) 482 uLogger.debug(" - request type: {}".format(reqType)) 483 uLogger.debug(" - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***"))) 484 uLogger.debug(" - body:\n{}".format(self.body)) 485 486 # fast hack to avoid all operations with some tickers/FIGI 487 responseJSON = {} 488 oK = True 489 for item in self.exclude: 490 if item in url: 491 if self.moreDebug: 492 uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude))) 493 494 oK = False 495 break 496 497 if oK: 498 with self.__lock: # acquire the mutex lock 499 counter = 0 500 response = None 501 errMsg = "" 502 503 while not response and counter <= retry: 504 if reqType == "GET": 505 response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout) 506 507 if reqType == "POST": 508 response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout) 509 510 if self.moreDebug: 511 uLogger.debug("Response:") 512 uLogger.debug(" - status code: {}".format(response.status_code)) 513 uLogger.debug(" - reason: {}".format(response.reason)) 514 uLogger.debug(" - body length: {}".format(len(response.text))) 515 uLogger.debug(" - headers:\n{}".format(response.headers)) 516 517 # Server returns some headers: 518 # - `x-ratelimit-limit` — shows the settings of the current user limit for this method. 519 # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute. 520 # - `x-ratelimit-reset` — time in seconds before resetting the request counter. 521 # See: https://tinkoff.github.io/investAPI/grpc/#kreya 522 if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0": 523 rateLimitWait = int(response.headers["x-ratelimit-reset"]) 524 uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait)) 525 sleep(rateLimitWait) 526 527 # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes 528 if 400 <= response.status_code < 500: 529 msg = "status code: [{}], response body: {}".format(response.status_code, response.text) 530 uLogger.debug(" - not oK, but do not retry for 4xx errors, {}".format(msg)) 531 532 if "code" in response.text and "message" in response.text: 533 msgDict = self._ParseJSON(rawData=response.text) 534 uLogger.warning("HTTP-status code [{}], server message: {}".format(response.status_code, msgDict["message"])) 535 536 counter = retry + 1 # do not retry for 4xx errors 537 538 if 500 <= response.status_code < 600: 539 errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text) 540 uLogger.debug(" - not oK, {}".format(errMsg)) 541 542 if "code" in response.text and "message" in response.text: 543 errMsgDict = self._ParseJSON(rawData=response.text) 544 uLogger.warning("HTTP-status code [{}], error message: {}".format(response.status_code, errMsgDict["message"])) 545 546 counter += 1 547 548 if counter <= retry: 549 uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause)) 550 sleep(pause) 551 552 responseJSON = self._ParseJSON(rawData=response.text) 553 554 if errMsg: 555 uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/") 556 uLogger.error(" - not oK, {}".format(errMsg)) 557 558 return responseJSON
Send GET or POST request to broker server and receive JSON object.
self.header: must be defining with dictionary of headers. self.body: if define then used as request body. None by default. self.timeout: global request timeout, 15 seconds by default.
Parameters
- url: url with REST request.
- reqType: send "GET" or "POST" request. "GET" by default.
- retry: how many times retry after first request if an 5xx server errors occurred.
- pause: sleep time in seconds between retries.
Returns
response JSON (dictionary) from broker.
591 def Listing(self) -> dict: 592 """ 593 Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server. 594 595 :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures. 596 """ 597 uLogger.debug("Requesting all available instruments for current account. Wait, please...") 598 uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES)) 599 600 # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService 601 # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list. 602 iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS] 603 604 poolUpdater = ThreadPool(processes=CPU_USAGES) # create pool for update instruments in parallel mode 605 listing = poolUpdater.map(self._IWrapper, iParams) # execute update operations 606 poolUpdater.close() # close the thread pool 607 poolUpdater.join() # wait a moment until all data returns from threads 608 609 # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures. 610 # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method 611 iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing} 612 613 # calculate minimum price increment (step) for all instruments and set up instrument's type: 614 for iType in iList.keys(): 615 for ticker in iList[iType]: 616 iList[iType][ticker]["type"] = iType 617 618 if "minPriceIncrement" in iList[iType][ticker].keys(): 619 iList[iType][ticker]["step"] = NanoToFloat( 620 iList[iType][ticker]["minPriceIncrement"]["units"], 621 iList[iType][ticker]["minPriceIncrement"]["nano"], 622 ) 623 624 else: 625 iList[iType][ticker]["step"] = 0 # hack to avoid empty value in some instruments, e.g. futures 626 627 return iList
Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
Returns
Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
629 def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None: 630 """ 631 Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics. 632 633 See also: `DumpInstruments()`, `Listing()`. 634 635 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 636 otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) . 637 """ 638 if self.iListDumpFile is None or not self.iListDumpFile: 639 uLogger.error("Output name of dump file must be defined!") 640 raise Exception("Filename required") 641 642 if not self.iList or forceUpdate: 643 self.iList = self.Listing() 644 645 xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx" 646 647 # Save as XLSX with separated sheets for every type of instruments: 648 with pd.ExcelWriter( 649 path=xlsxDumpFile, 650 date_format=TKS_DATE_FORMAT, 651 datetime_format=TKS_DATE_TIME_FORMAT, 652 mode="w", 653 ) as writer: 654 for iType in TKS_INSTRUMENTS: 655 df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index") # generate pandas object from self.iList dictionary 656 df = df[sorted(df)] # sorted by column names 657 df = df.applymap( 658 lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item, 659 na_action="ignore", 660 ) # converting numbers from nano-type to float in every cell 661 df.to_excel( 662 writer, 663 sheet_name=iType, 664 encoding="UTF-8", 665 freeze_panes=(1, 1), 666 ) # saving as XLSX-file with freeze first row and column as headers 667 668 uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))
Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
See also: DumpInstruments(), Listing().
Parameters
670 def DumpInstruments(self, forceUpdate: bool = True) -> str: 671 """ 672 Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server 673 using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file. 674 675 See also: `DumpInstrumentsAsXLSX()`, `Listing()`. 676 677 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 678 otherwise just saves exist `iList` as JSON-file (default: `dump.json`). 679 :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file. 680 """ 681 if self.iListDumpFile is None or not self.iListDumpFile: 682 uLogger.error("Output name of dump file must be defined!") 683 raise Exception("Filename required") 684 685 if not self.iList or forceUpdate: 686 self.iList = self.Listing() 687 688 jsonDump = json.dumps(self.iList, indent=4, sort_keys=False) # create JSON object as string 689 with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH: 690 fH.write(jsonDump) 691 692 uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile))) 693 694 return jsonDump
Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
using Listing() method. If iListDumpFile string is not empty then also save information to this file.
See also: DumpInstrumentsAsXLSX(), Listing().
Parameters
- forceUpdate: if
Truethen at first updates data withListing()method, otherwise just saves existiListas JSON-file (default:dump.json).
Returns
serialized JSON formatted
strwith full data of instruments, also saved to the--outputJSON-file.
696 def ShowInstrumentInfo(self, iJSON: dict, show: bool = True, onlyFiles=False) -> str: 697 """ 698 Show information about one instrument defined by json data and prints it in Markdown format. 699 700 See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`. 701 702 :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self._ticker]` 703 :param show: if `True` then also printing information about instrument and its current price. 704 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 705 :return: multilines text in Markdown format with information about one instrument. 706 """ 707 splitLine = "| | |\n" 708 infoText = "" 709 710 if iJSON is not None and iJSON and isinstance(iJSON, dict): 711 info = [ 712 "# Main information\n\n", 713 "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 714 "| Parameters | Values |\n", 715 "|-------------------------------------------------------------|--------------------------------------------------------|\n", 716 "| Ticker: | {:<54} |\n".format(iJSON["ticker"]), 717 "| Full name: | {:<54} |\n".format(iJSON["name"]), 718 ] 719 720 if "sector" in iJSON.keys() and iJSON["sector"]: 721 info.append("| Sector: | {:<54} |\n".format(iJSON["sector"])) 722 723 if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]: 724 info.append("| Country of instrument: | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"]))) 725 726 info.extend([ 727 splitLine, 728 "| FIGI (Financial Instrument Global Identifier): | {:<54} |\n".format(iJSON["figi"]), 729 "| Real exchange [Exchange section]: | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])), 730 ]) 731 732 if "isin" in iJSON.keys() and iJSON["isin"]: 733 info.append("| ISIN (International Securities Identification Number): | {:<54} |\n".format(iJSON["isin"])) 734 735 if "classCode" in iJSON.keys(): 736 info.append("| Class Code (exchange section where instrument is traded): | {:<54} |\n".format(iJSON["classCode"])) 737 738 info.extend([ 739 splitLine, 740 "| Current broker security trading status: | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]), 741 splitLine, 742 "| Buy operations allowed: | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"), 743 "| Sale operations allowed: | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"), 744 "| Short positions allowed: | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"), 745 ]) 746 747 if iJSON["figi"]: 748 self._figi = iJSON["figi"] 749 iJSON = iJSON | self.RequestTradingStatus() 750 751 info.extend([ 752 splitLine, 753 "| Limit orders allowed: | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"), 754 "| Market orders allowed: | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"), 755 "| API trade allowed: | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"), 756 ]) 757 758 info.append(splitLine) 759 760 if "type" in iJSON.keys() and iJSON["type"]: 761 info.append("| Type of the instrument: | {:<54} |\n".format(iJSON["type"])) 762 763 if "shareType" in iJSON.keys() and iJSON["shareType"]: 764 info.append("| Share type: | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]])) 765 766 if "futuresType" in iJSON.keys() and iJSON["futuresType"]: 767 info.append("| Futures type: | {:<54} |\n".format(iJSON["futuresType"])) 768 769 if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]: 770 info.append("| IPO date: | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", ""))) 771 772 if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]: 773 info.append("| Released date: | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", ""))) 774 775 if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]: 776 info.append("| Rebalancing frequency: | {:<54} |\n".format(iJSON["rebalancingFreq"])) 777 778 if "focusType" in iJSON.keys() and iJSON["focusType"]: 779 info.append("| Focusing type: | {:<54} |\n".format(iJSON["focusType"])) 780 781 if "assetType" in iJSON.keys() and iJSON["assetType"]: 782 info.append("| Asset type: | {:<54} |\n".format(iJSON["assetType"])) 783 784 if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]: 785 info.append("| Basic asset: | {:<54} |\n".format(iJSON["basicAsset"])) 786 787 if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]: 788 info.append("| Basic asset size: | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"])))) 789 790 if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]: 791 info.append("| ISO currency name: | {:<54} |\n".format(iJSON["isoCurrencyName"])) 792 793 if "currency" in iJSON.keys(): 794 info.append("| Payment currency: | {:<54} |\n".format(iJSON["currency"])) 795 796 if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys(): 797 info.append("| Nominal currency: | {:<54} |\n".format(iJSON["nominal"]["currency"])) 798 799 if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]: 800 info.append("| First trade date: | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", ""))) 801 802 if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]: 803 info.append("| Last trade date: | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", ""))) 804 805 if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]: 806 info.append("| Date of expiration: | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", ""))) 807 808 if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]: 809 info.append("| State registration date: | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", ""))) 810 811 if "placementDate" in iJSON.keys() and iJSON["placementDate"]: 812 info.append("| Placement date: | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", ""))) 813 814 if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]: 815 info.append("| Maturity date: | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", ""))) 816 817 if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]: 818 info.append("| Perpetual bond: | Yes |\n") 819 820 if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]: 821 info.append("| Over-the-counter (OTC) securities: | Yes |\n") 822 823 iExt = None 824 if iJSON["type"] == "Bonds": 825 info.extend([ 826 splitLine, 827 "| Bond issue (size / plan): | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])), 828 "| Nominal price (100%): | {:<54} |\n".format("{} {}".format( 829 "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."), 830 iJSON["nominal"]["currency"], 831 )), 832 ]) 833 834 if "floatingCouponFlag" in iJSON.keys(): 835 info.append("| Floating coupon: | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No")) 836 837 if "amortizationFlag" in iJSON.keys(): 838 info.append("| Amortization: | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No")) 839 840 info.append(splitLine) 841 842 if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]: 843 info.append("| Number of coupon payments per year: | {:<54} |\n".format(iJSON["couponQuantityPerYear"])) 844 845 if iJSON["figi"]: 846 iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False) # extended bonds data 847 848 info.extend([ 849 "| Days last to maturity date: | {:<54} |\n".format(iExt["daysToMaturity"][0]), 850 "| Coupons yield (average coupon daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])), 851 "| Current price yield (average daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])), 852 ]) 853 854 if "aciValue" in iJSON.keys() and iJSON["aciValue"]: 855 info.append("| Current accumulated coupon income (ACI): | {:<54} |\n".format("{:.2f} {}".format( 856 NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]), 857 iJSON["aciValue"]["currency"] 858 ))) 859 860 if "currentPrice" in iJSON.keys(): 861 info.append(splitLine) 862 863 currency = iJSON["currency"] if "currency" in iJSON.keys() else "" # nominal currency for bonds, otherwise currency of instrument 864 aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else "" # payment currency 865 866 bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0 # previous close price of bond 867 bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0 # last price of bond 868 bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0 # max price of bond 869 bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0 # min price of bond 870 bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0 # delta between last deal price and last close 871 872 curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0 873 curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0 874 875 info.extend([ 876 "| Previous close price of the instrument: | {:<54} |\n".format("{}{}".format( 877 "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A", 878 "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 879 )), 880 "| Last deal price of the instrument: | {:<54} |\n".format("{}{}".format( 881 "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A", 882 "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 883 )), 884 "| Changes between last deal price and last close | {:<54} |\n".format( 885 "{:.2f}%{}".format( 886 iJSON["currentPrice"]["changes"], 887 " ({}{:.2f} {})".format( 888 "+" if bondChangesDelta > 0 else "", 889 bondChangesDelta, 890 aciCurrency 891 ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format( 892 "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "", 893 iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"], 894 currency 895 ), 896 ) 897 ), 898 "| Current limit price, min / max: | {:<54} |\n".format("{}{} / {}{}{}".format( 899 "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A", 900 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 901 "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A", 902 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 903 " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else "" 904 )), 905 "| Actual price, sell / buy: | {:<54} |\n".format("{}{} / {}{}{}".format( 906 "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A", 907 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 908 "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A", 909 "%" if iJSON["type"] == "Bonds" else" {}".format(currency), 910 " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else "" 911 )), 912 ]) 913 914 if "lot" in iJSON.keys(): 915 info.append("| Minimum lot to buy: | {:<54} |\n".format(iJSON["lot"])) 916 917 if "step" in iJSON.keys() and iJSON["step"] != 0: 918 info.append("| Minimum price increment (step): | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else ""))) 919 920 # Add bond payment calendar: 921 if iJSON["type"] == "Bonds": 922 strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False) # bond payment calendar 923 info.extend(["\n#", strCalendar]) 924 925 infoText += "".join(info) 926 927 if show and not onlyFiles: 928 uLogger.info("{}".format(infoText)) 929 930 if self.infoFile is not None and (show or onlyFiles): 931 with open(self.infoFile, "w", encoding="UTF-8") as fH: 932 fH.write(infoText) 933 934 uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile))) 935 936 if self.useHTMLReports: 937 htmlFilePath = self.infoFile.replace(".md", ".html") if self.infoFile.endswith(".md") else self.infoFile + ".html" 938 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 939 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Main information", commonCSS=COMMON_CSS, markdown=infoText)) 940 941 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 942 943 return infoText
Show information about one instrument defined by json data and prints it in Markdown format.
See also: SearchByTicker(), SearchByFIGI(), RequestBondCoupons(), ExtendBondsData(), ShowBondsCalendar() and RequestTradingStatus().
Parameters
- iJSON: json data of instrument, example:
iJSON = self.iList["Shares"][self._ticker] - show: if
Truethen also printing information about instrument and its current price. - onlyFiles: if
Truethen do not show Markdown table in the console, but only generates report files.
Returns
multilines text in Markdown format with information about one instrument.
945 def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict: 946 """ 947 Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined! 948 949 :param requestPrice: if `False` then do not request current price of instrument (because this is long operation). 950 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 951 :return: JSON formatted data with information about instrument. 952 """ 953 tickerJSON = {} 954 if self.moreDebug: 955 uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self._ticker)) 956 957 if not self._ticker: 958 uLogger.warning("self._ticker variable is not be empty!") 959 960 else: 961 if self._ticker in TKS_TICKERS_OR_FIGI_EXCLUDED: 962 uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self._ticker)) 963 raise Exception("Instrument not allowed") 964 965 if not self.iList: 966 self.iList = self.Listing() 967 968 if self._ticker in self.iList["Shares"].keys(): 969 tickerJSON = self.iList["Shares"][self._ticker] 970 if self.moreDebug: 971 uLogger.debug("Ticker [{}] found in shares list".format(self._ticker)) 972 973 elif self._ticker in self.iList["Currencies"].keys(): 974 tickerJSON = self.iList["Currencies"][self._ticker] 975 if self.moreDebug: 976 uLogger.debug("Ticker [{}] found in currencies list".format(self._ticker)) 977 978 elif self._ticker in self.iList["Bonds"].keys(): 979 tickerJSON = self.iList["Bonds"][self._ticker] 980 if self.moreDebug: 981 uLogger.debug("Ticker [{}] found in bonds list".format(self._ticker)) 982 983 elif self._ticker in self.iList["Etfs"].keys(): 984 tickerJSON = self.iList["Etfs"][self._ticker] 985 if self.moreDebug: 986 uLogger.debug("Ticker [{}] found in etfs list".format(self._ticker)) 987 988 elif self._ticker in self.iList["Futures"].keys(): 989 tickerJSON = self.iList["Futures"][self._ticker] 990 if self.moreDebug: 991 uLogger.debug("Ticker [{}] found in futures list".format(self._ticker)) 992 993 if tickerJSON: 994 self._figi = tickerJSON["figi"] 995 996 if requestPrice: 997 tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False) 998 999 if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None: 1000 tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"] 1001 1002 else: 1003 tickerJSON["currentPrice"]["changes"] = 0 1004 1005 if show: 1006 self.ShowInstrumentInfo(iJSON=tickerJSON, show=True) # print info as Markdown text 1007 1008 else: 1009 if show: 1010 uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self._ticker)) 1011 1012 return tickerJSON
Search and return raw broker's information about instrument by its ticker. Variable ticker must be defined!
Parameters
- requestPrice: if
Falsethen do not request current price of instrument (because this is long operation). - show: if
Falsethen do not runShowInstrumentInfo()method and do not print info to the console.
Returns
JSON formatted data with information about instrument.
1014 def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict: 1015 """ 1016 Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined! 1017 1018 :param requestPrice: if `False` then do not request current price of instrument (it's long operation). 1019 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 1020 :return: JSON formatted data with information about instrument. 1021 """ 1022 figiJSON = {} 1023 if self.moreDebug: 1024 uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self._figi)) 1025 1026 if not self._figi: 1027 uLogger.warning("self._figi variable is not be empty!") 1028 1029 else: 1030 if self._figi in TKS_TICKERS_OR_FIGI_EXCLUDED: 1031 uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self._figi)) 1032 raise Exception("Instrument not allowed") 1033 1034 if not self.iList: 1035 self.iList = self.Listing() 1036 1037 for item in self.iList["Shares"].keys(): 1038 if self._figi == self.iList["Shares"][item]["figi"]: 1039 figiJSON = self.iList["Shares"][item] 1040 1041 if self.moreDebug: 1042 uLogger.debug("FIGI [{}] found in shares list".format(self._figi)) 1043 1044 break 1045 1046 if not figiJSON: 1047 for item in self.iList["Currencies"].keys(): 1048 if self._figi == self.iList["Currencies"][item]["figi"]: 1049 figiJSON = self.iList["Currencies"][item] 1050 1051 if self.moreDebug: 1052 uLogger.debug("FIGI [{}] found in currencies list".format(self._figi)) 1053 1054 break 1055 1056 if not figiJSON: 1057 for item in self.iList["Bonds"].keys(): 1058 if self._figi == self.iList["Bonds"][item]["figi"]: 1059 figiJSON = self.iList["Bonds"][item] 1060 1061 if self.moreDebug: 1062 uLogger.debug("FIGI [{}] found in bonds list".format(self._figi)) 1063 1064 break 1065 1066 if not figiJSON: 1067 for item in self.iList["Etfs"].keys(): 1068 if self._figi == self.iList["Etfs"][item]["figi"]: 1069 figiJSON = self.iList["Etfs"][item] 1070 1071 if self.moreDebug: 1072 uLogger.debug("FIGI [{}] found in etfs list".format(self._figi)) 1073 1074 break 1075 1076 if not figiJSON: 1077 for item in self.iList["Futures"].keys(): 1078 if self._figi == self.iList["Futures"][item]["figi"]: 1079 figiJSON = self.iList["Futures"][item] 1080 1081 if self.moreDebug: 1082 uLogger.debug("FIGI [{}] found in futures list".format(self._figi)) 1083 1084 break 1085 1086 if figiJSON: 1087 self._figi = figiJSON["figi"] 1088 self._ticker = figiJSON["ticker"] 1089 1090 if requestPrice: 1091 figiJSON["currentPrice"] = self.GetCurrentPrices(show=False) 1092 1093 if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None: 1094 figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"] 1095 1096 else: 1097 figiJSON["currentPrice"]["changes"] = 0 1098 1099 if show: 1100 self.ShowInstrumentInfo(iJSON=figiJSON, show=True) # print info as Markdown text 1101 1102 else: 1103 if show: 1104 uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self._figi)) 1105 1106 return figiJSON
Search and return raw broker's information about instrument by its FIGI. Variable figi must be defined!
Parameters
- requestPrice: if
Falsethen do not request current price of instrument (it's long operation). - show: if
Falsethen do not runShowInstrumentInfo()method and do not print info to the console.
Returns
JSON formatted data with information about instrument.
1108 def GetCurrentPrices(self, show: bool = True) -> dict: 1109 """ 1110 Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5: 1111 `{"buy": [{"price": 1243.8, "quantity": 193}, 1112 {"price": 1244.0, "quantity": 168}, 1113 {"price": 1244.8, "quantity": 5}, 1114 {"price": 1245.0, "quantity": 61}, 1115 {"price": 1245.4, "quantity": 60}], 1116 "sell": [{"price": 1243.6, "quantity": 8}, 1117 {"price": 1242.6, "quantity": 10}, 1118 {"price": 1242.4, "quantity": 18}, 1119 {"price": 1242.2, "quantity": 50}, 1120 {"price": 1242.0, "quantity": 113}], 1121 "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean: 1122 - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order 1123 - sell: list of dicts with Buyers prices, 1124 - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument), 1125 - quantity: volume value by current price in lots, 1126 - limitUp: current trade session limit price, maximum, 1127 - limitDown: current trade session limit price, minimum, 1128 - lastPrice: last deal price of the instrument, 1129 - closePrice: previous trade session close price of the instrument. 1130 1131 See also: `SearchByTicker()` and `SearchByFIGI()`. 1132 REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1133 Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1134 1135 :param show: if `True` then print DOM to log and console. 1136 :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`. 1137 If an error occurred then returns an empty record: 1138 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`. 1139 """ 1140 prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0} 1141 1142 if self.depth < 1: 1143 uLogger.error("Depth of Market (DOM) must be >=1!") 1144 raise Exception("Incorrect value") 1145 1146 if not (self._ticker or self._figi): 1147 uLogger.error("self._ticker or self._figi variables must be defined!") 1148 raise Exception("Ticker or FIGI required") 1149 1150 if self._ticker and not self._figi: 1151 instrumentByTicker = self.SearchByTicker(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1152 self._figi = instrumentByTicker["figi"] if instrumentByTicker else "" 1153 1154 if not self._ticker and self._figi: 1155 instrumentByFigi = self.SearchByFIGI(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1156 self._ticker = instrumentByFigi["ticker"] if instrumentByFigi else "" 1157 1158 if not self._figi: 1159 uLogger.error("FIGI is not defined!") 1160 raise Exception("Ticker or FIGI required") 1161 1162 else: 1163 uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self._ticker, self._figi)) 1164 1165 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1166 priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook" 1167 self.body = str({"figi": self._figi, "depth": self.depth}) 1168 pricesResponse = self.SendAPIRequest(priceURL, reqType="POST") # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1169 1170 if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()): 1171 # list of dicts with sellers orders: 1172 prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]] 1173 1174 # list of dicts with buyers orders: 1175 prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]] 1176 1177 # max price of instrument at this time: 1178 prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None 1179 1180 # min price of instrument at this time: 1181 prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None 1182 1183 # last price of deal with instrument: 1184 prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0 1185 1186 # last close price of instrument: 1187 prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0 1188 1189 else: 1190 uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi)) 1191 uLogger.debug("Server response: {}".format(pricesResponse)) 1192 1193 if show: 1194 if prices["buy"] or prices["sell"]: 1195 info = [ 1196 "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format( 1197 datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 1198 self._ticker, 1199 self._figi, 1200 self.depth, 1201 ), 1202 "-" * 60, "\n", 1203 " Orders of Buyers | Orders of Sellers\n", 1204 "-" * 60, "\n", 1205 " Sell prices (volumes) | Buy prices (volumes)\n", 1206 "-" * 60, "\n", 1207 ] 1208 1209 if not prices["buy"]: 1210 info.append(" | No orders!\n") 1211 sumBuy = 0 1212 1213 else: 1214 sumBuy = sum([x["quantity"] for x in prices["buy"]]) 1215 maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True) 1216 for item in maxMinSorted: 1217 info.append(" | {} ({})\n".format(item["price"], item["quantity"])) 1218 1219 if not prices["sell"]: 1220 info.append("No orders! |\n") 1221 sumSell = 0 1222 1223 else: 1224 sumSell = sum([x["quantity"] for x in prices["sell"]]) 1225 for item in prices["sell"]: 1226 info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"]))) 1227 1228 info.extend([ 1229 "-" * 60, "\n", 1230 "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)), 1231 "-" * 60, "\n", 1232 ]) 1233 1234 infoText = "".join(info) 1235 1236 uLogger.info("Current prices in order book:\n\n{}".format(infoText)) 1237 1238 else: 1239 uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi)) 1240 1241 return prices
Get and show Depth of Market with current prices of the instrument as dictionary. Result example with depth 5:
{"buy": [{"price": 1243.8, "quantity": 193},
{"price": 1244.0, "quantity": 168},
{"price": 1244.8, "quantity": 5},
{"price": 1245.0, "quantity": 61},
{"price": 1245.4, "quantity": 60}],
"sell": [{"price": 1243.6, "quantity": 8},
{"price": 1242.6, "quantity": 10},
{"price": 1242.4, "quantity": 18},
{"price": 1242.2, "quantity": 50},
{"price": 1242.0, "quantity": 113}],
"limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}, where parameters mean:
- buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
- sell: list of dicts with Buyers prices,
- price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
- quantity: volume value by current price in lots,
- limitUp: current trade session limit price, maximum,
- limitDown: current trade session limit price, minimum,
- lastPrice: last deal price of the instrument,
- closePrice: previous trade session close price of the instrument.
See also: SearchByTicker() and SearchByFIGI().
REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
Parameters
- show: if
Truethen print DOM to log and console.
Returns
orders book dict with lists of current buy and sell prices:
{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}. If an error occurred then returns an empty record:{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}.
1243 def ShowInstrumentsInfo(self, show: bool = True, onlyFiles=False) -> str: 1244 """ 1245 This method get and show information about all available broker instruments for current user account. 1246 If `instrumentsFile` string is not empty then also save information to this file. 1247 1248 :param show: if `True` then print results to console, if `False` — print only to file. 1249 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 1250 :return: multi-lines string with all available broker instruments. 1251 """ 1252 if not self.iList: 1253 self.iList = self.Listing() 1254 1255 info = [ 1256 "# All available instruments from Tinkoff Broker server for current user token\n\n", 1257 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 1258 ] 1259 1260 # add instruments count by type: 1261 for iType in self.iList.keys(): 1262 info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType]))) 1263 1264 headerLine = "| Ticker | Full name | FIGI | Cur | Lot | Step |\n" 1265 splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n" 1266 1267 # generating info tables with all instruments by type: 1268 for iType in self.iList.keys(): 1269 info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine]) 1270 1271 for instrument in self.iList[iType].keys(): 1272 iName = self.iList[iType][instrument]["name"] # instrument's name 1273 if len(iName) > 57: 1274 iName = "{}...".format(iName[:54]) # right trim for a long string 1275 1276 info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format( 1277 self.iList[iType][instrument]["ticker"], 1278 iName, 1279 self.iList[iType][instrument]["figi"], 1280 self.iList[iType][instrument]["currency"], 1281 self.iList[iType][instrument]["lot"], 1282 "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0, 1283 )) 1284 1285 infoText = "".join(info) 1286 1287 if show and not onlyFiles: 1288 uLogger.info(infoText) 1289 1290 if self.instrumentsFile and (show or onlyFiles): 1291 with open(self.instrumentsFile, "w", encoding="UTF-8") as fH: 1292 fH.write(infoText) 1293 1294 uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile))) 1295 1296 if self.useHTMLReports: 1297 htmlFilePath = self.instrumentsFile.replace(".md", ".html") if self.instrumentsFile.endswith(".md") else self.instrumentsFile + ".html" 1298 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1299 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="List of instruments", commonCSS=COMMON_CSS, markdown=infoText)) 1300 1301 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1302 1303 return infoText
This method get and show information about all available broker instruments for current user account.
If instrumentsFile string is not empty then also save information to this file.
Parameters
- show: if
Truethen print results to console, ifFalse— print only to file. - onlyFiles: if
Truethen do not show Markdown table in the console, but only generates report files.
Returns
multi-lines string with all available broker instruments.
1305 def SearchInstruments(self, pattern: str, show: bool = True, onlyFiles=False) -> dict: 1306 """ 1307 This method search and show information about instruments by part of its ticker, FIGI or name. 1308 If `searchResultsFile` string is not empty then also save information to this file. 1309 1310 :param pattern: string with part of ticker, FIGI or instrument's name. 1311 :param show: if `True` then print results to console, if `False` — return list of result only. 1312 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 1313 :return: list of dictionaries with all found instruments. 1314 """ 1315 if not self.iList: 1316 self.iList = self.Listing() 1317 1318 searchResults = {iType: {} for iType in self.iList} # same as iList but will contain only filtered instruments 1319 compiledPattern = re.compile(pattern, re.IGNORECASE) 1320 1321 for iType in self.iList: 1322 for instrument in self.iList[iType].values(): 1323 searchResult = compiledPattern.search(" ".join( 1324 [instrument["ticker"], instrument["figi"], instrument["name"]] 1325 )) 1326 1327 if searchResult: 1328 searchResults[iType][instrument["ticker"]] = instrument 1329 1330 resultsLen = sum([len(searchResults[iType]) for iType in searchResults]) 1331 info = [ 1332 "# Search results\n\n", 1333 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 1334 "* **Search pattern:** [{}]\n".format(pattern), 1335 "* **Found instruments:** [{}]\n\n".format(resultsLen), 1336 '**Note:** you can view info about found instruments with key "--info", e.g.: "tksbrokerapi -t TICKER --info" or "tksbrokerapi -f FIGI --info".\n' 1337 ] 1338 infoShort = info[:] 1339 1340 headerLine = "| Type | Ticker | Full name | FIGI |\n" 1341 splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n" 1342 skippedLine = "| ... | ... | ... | ... |\n" 1343 1344 if resultsLen == 0: 1345 info.append("\nNo results\n") 1346 infoShort.append("\nNo results\n") 1347 uLogger.warning("No results. Try changing your search pattern.") 1348 1349 else: 1350 for iType in searchResults: 1351 iTypeValuesCount = len(searchResults[iType].values()) 1352 if iTypeValuesCount > 0: 1353 info.extend(["\n## {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1354 infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1355 1356 for instrument in searchResults[iType].values(): 1357 info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format( 1358 instrument["type"], 1359 instrument["ticker"], 1360 "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"], # right trim for a long string 1361 instrument["figi"], 1362 )) 1363 1364 if iTypeValuesCount <= 5: 1365 infoShort.extend(info[-iTypeValuesCount:]) 1366 1367 else: 1368 infoShort.extend(info[-5:]) 1369 infoShort.append(skippedLine) 1370 1371 infoText = "".join(info) 1372 infoTextShort = "".join(infoShort) 1373 1374 if show and not onlyFiles: 1375 uLogger.info(infoTextShort) 1376 uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`") 1377 1378 if self.searchResultsFile and (show or onlyFiles): 1379 with open(self.searchResultsFile, "w", encoding="UTF-8") as fH: 1380 fH.write(infoText) 1381 1382 uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile))) 1383 1384 if self.useHTMLReports: 1385 htmlFilePath = self.searchResultsFile.replace(".md", ".html") if self.searchResultsFile.endswith(".md") else self.searchResultsFile + ".html" 1386 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1387 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Search results", commonCSS=COMMON_CSS, markdown=infoText)) 1388 1389 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1390 1391 return searchResults
This method search and show information about instruments by part of its ticker, FIGI or name.
If searchResultsFile string is not empty then also save information to this file.
Parameters
- pattern: string with part of ticker, FIGI or instrument's name.
- show: if
Truethen print results to console, ifFalse— return list of result only. - onlyFiles: if
Truethen do not show Markdown table in the console, but only generates report files.
Returns
list of dictionaries with all found instruments.
1393 def GetUniqueFIGIs(self, instruments: list[str]) -> list: 1394 """ 1395 Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs. 1396 1397 :param instruments: list of strings with tickers or FIGIs. 1398 :return: list with unique instrument FIGIs only. 1399 """ 1400 requestedInstruments = [] 1401 for iName in instruments: 1402 if iName not in self.aliases.keys(): 1403 if iName not in requestedInstruments: 1404 requestedInstruments.append(iName) 1405 1406 else: 1407 if iName not in requestedInstruments: 1408 if self.aliases[iName] not in requestedInstruments: 1409 requestedInstruments.append(self.aliases[iName]) 1410 1411 uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments)) 1412 1413 onlyUniqueFIGIs = [] 1414 for iName in requestedInstruments: 1415 if iName in TKS_TICKERS_OR_FIGI_EXCLUDED: 1416 continue 1417 1418 self._ticker = iName 1419 iData = self.SearchByTicker(requestPrice=False) # trying to find instrument by ticker 1420 1421 if not iData: 1422 self._ticker = "" 1423 self._figi = iName 1424 1425 iData = self.SearchByFIGI(requestPrice=False) # trying to find instrument by FIGI 1426 1427 if not iData: 1428 self._figi = "" 1429 uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName)) 1430 1431 if iData and iData["figi"] not in onlyUniqueFIGIs: 1432 onlyUniqueFIGIs.append(iData["figi"]) 1433 1434 uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs)) 1435 1436 return onlyUniqueFIGIs
Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs.
Parameters
- instruments: list of strings with tickers or FIGIs.
Returns
list with unique instrument FIGIs only.
1438 def GetListOfPrices(self, instruments: list[str], show: bool = False, onlyFiles=False) -> list[dict]: 1439 """ 1440 This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation! 1441 1442 See limits: https://tinkoff.github.io/investAPI/limits/ 1443 1444 If `pricesFile` string is not empty then also save information to this file. 1445 1446 :param instruments: list of strings with tickers or FIGIs. 1447 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1448 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 1449 :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1450 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods. 1451 """ 1452 if instruments is None or not instruments: 1453 uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!") 1454 raise Exception("Ticker or FIGI required") 1455 1456 onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments) 1457 1458 uLogger.debug("Requesting current prices from Tinkoff Broker server...") 1459 1460 iList = [] # trying to get info and current prices about all unique instruments: 1461 for self._figi in onlyUniqueFIGIs: 1462 iData = self.SearchByFIGI(requestPrice=True, show=False) 1463 iList.append(iData) 1464 1465 self.ShowListOfPrices(iList, show, onlyFiles) 1466 1467 return iList
This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
See limits: https://tinkoff.github.io/investAPI/limits/
If pricesFile string is not empty then also save information to this file.
Parameters
- instruments: list of strings with tickers or FIGIs.
- show: if
Truethen prints prices to console, ifFalse— prints only to filepricesFile. - onlyFiles: if
Truethen do not show Markdown table in the console, but only generates report files.
Returns
list of instruments looks like
[{some ticker info, "currentPrice": {current prices}}, {...}, ...]. One item is dict returned bySearchByTicker()orSearchByFIGI()methods.
1469 def ShowListOfPrices(self, iList: list, show: bool = True, onlyFiles=False) -> str: 1470 """ 1471 Show table contains current prices of given instruments. 1472 1473 :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1474 One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods. 1475 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1476 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 1477 :return: multilines text in Markdown format as a table contains current prices. 1478 """ 1479 infoText = "" 1480 1481 if show or self.pricesFile or onlyFiles: 1482 info = [ 1483 "# Current prices\n\n* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1484 "| Ticker | FIGI | Type | Prev. close | Last price | Chg. % | Day limits min/max | Actual sell / buy | Curr. |\n", 1485 "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n", 1486 ] 1487 1488 for item in iList: 1489 info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format( 1490 item["ticker"], 1491 item["figi"], 1492 item["type"], 1493 "{:.2f}".format(float(item["currentPrice"]["closePrice"])), 1494 "{:.2f}".format(float(item["currentPrice"]["lastPrice"])), 1495 "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])), 1496 "{} / {}".format( 1497 item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A", 1498 item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A", 1499 ), 1500 "{} / {}".format( 1501 item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A", 1502 item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A", 1503 ), 1504 item["currency"], 1505 )) 1506 1507 infoText = "".join(info) 1508 1509 if show and not onlyFiles: 1510 uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText)) 1511 1512 if self.pricesFile and (show or onlyFiles): 1513 with open(self.pricesFile, "w", encoding="UTF-8") as fH: 1514 fH.write(infoText) 1515 1516 uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile))) 1517 1518 if self.useHTMLReports: 1519 htmlFilePath = self.pricesFile.replace(".md", ".html") if self.pricesFile.endswith(".md") else self.pricesFile + ".html" 1520 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1521 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Current prices", commonCSS=COMMON_CSS, markdown=infoText)) 1522 1523 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1524 1525 return infoText
Show table contains current prices of given instruments.
Parameters
- **iList: list of instruments looks like
[{some ticker info, "currentPrice"**: {current prices}}, {...}, ...]. One item is dict returned bySearchByTicker(requestPrice=True)or bySearchByFIGI(requestPrice=True)methods. - show: if
Truethen prints prices to console, ifFalse— prints only to filepricesFile. - onlyFiles: if
Truethen do not show Markdown table in the console, but only generates report files.
Returns
multilines text in Markdown format as a table contains current prices.
1527 def RequestTradingStatus(self) -> dict: 1528 """ 1529 Requesting trading status for the instrument defined by `figi` variable. 1530 1531 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus 1532 1533 Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest 1534 1535 :return: dictionary with trading status attributes. Response example: 1536 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", 1537 "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}` 1538 """ 1539 if self._figi is None or not self._figi: 1540 uLogger.error("Variable `figi` must be defined for using this method!") 1541 raise Exception("FIGI required") 1542 1543 uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self._figi)) 1544 1545 self.body = str({"figi": self._figi, "instrumentId": self._figi}) 1546 tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus" 1547 tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST") 1548 1549 if self.moreDebug: 1550 uLogger.debug("Records about current trading status successfully received") 1551 1552 return tradingStatus
Requesting trading status for the instrument defined by figi variable.
Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
Returns
dictionary with trading status attributes. Response example:
{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}
1554 def RequestPortfolio(self) -> dict: 1555 """ 1556 Requesting actual user's portfolio for current `accountId`. 1557 1558 REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio 1559 1560 Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest 1561 1562 :return: dictionary with user's portfolio. 1563 """ 1564 if self.accountId is None or not self.accountId: 1565 uLogger.error("Variable `accountId` must be defined for using this method!") 1566 raise Exception("Account ID required") 1567 1568 uLogger.debug("Requesting current actual user's portfolio. Wait, please...") 1569 1570 self.body = str({"accountId": self.accountId}) 1571 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio" 1572 rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST") 1573 1574 if self.moreDebug: 1575 uLogger.debug("Records about user's portfolio successfully received") 1576 1577 return rawPortfolio
Requesting actual user's portfolio for current accountId.
REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
Returns
dictionary with user's portfolio.
1579 def RequestPositions(self) -> dict: 1580 """ 1581 Requesting open positions by currencies and instruments for current `accountId`. 1582 1583 REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions 1584 1585 Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest 1586 1587 :return: dictionary with open positions by instruments. 1588 """ 1589 if self.accountId is None or not self.accountId: 1590 uLogger.error("Variable `accountId` must be defined for using this method!") 1591 raise Exception("Account ID required") 1592 1593 uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...") 1594 1595 self.body = str({"accountId": self.accountId}) 1596 positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions" 1597 rawPositions = self.SendAPIRequest(positionsURL, reqType="POST") 1598 1599 if self.moreDebug: 1600 uLogger.debug("Records about current open positions successfully received") 1601 1602 return rawPositions
Requesting open positions by currencies and instruments for current accountId.
REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
Returns
dictionary with open positions by instruments.
1604 def RequestPendingOrders(self) -> list: 1605 """ 1606 Requesting current actual pending limit orders for current `accountId`. 1607 1608 REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders 1609 1610 Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest 1611 1612 :return: list of dictionaries with pending limit orders. 1613 """ 1614 if self.accountId is None or not self.accountId: 1615 uLogger.error("Variable `accountId` must be defined for using this method!") 1616 raise Exception("Account ID required") 1617 1618 uLogger.debug("Requesting current actual pending limit orders. Wait, please...") 1619 1620 self.body = str({"accountId": self.accountId}) 1621 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders" 1622 rawResponse = self.SendAPIRequest(ordersURL, reqType="POST") 1623 1624 if "orders" in rawResponse.keys(): 1625 rawOrders = rawResponse["orders"] 1626 uLogger.debug("[{}] records about pending limit orders received".format(len(rawOrders))) 1627 1628 else: 1629 rawOrders = [] 1630 uLogger.debug("No pending limit orders returned! rawResponse = {}".format(rawResponse)) 1631 1632 return rawOrders
Requesting current actual pending limit orders for current accountId.
REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
Returns
list of dictionaries with pending limit orders.
1634 def RequestStopOrders(self) -> list: 1635 """ 1636 Requesting current actual stop orders for current `accountId`. 1637 1638 REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders 1639 1640 Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest 1641 1642 :return: list of dictionaries with stop orders. 1643 """ 1644 if self.accountId is None or not self.accountId: 1645 uLogger.error("Variable `accountId` must be defined for using this method!") 1646 raise Exception("Account ID required") 1647 1648 uLogger.debug("Requesting current actual stop orders. Wait, please...") 1649 1650 self.body = str({"accountId": self.accountId}) 1651 stopOrdersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders" 1652 rawResponse = self.SendAPIRequest(stopOrdersURL, reqType="POST") 1653 1654 if "stopOrders" in rawResponse.keys(): 1655 rawStopOrders = rawResponse["stopOrders"] 1656 uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders))) 1657 1658 else: 1659 rawStopOrders = [] 1660 uLogger.debug("No stop orders returned! rawResponse = {}".format(rawResponse)) 1661 1662 return rawStopOrders
Requesting current actual stop orders for current accountId.
REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
Returns
list of dictionaries with stop orders.
1664 def Overview(self, show: bool = False, details: str = "full", onlyFiles=False) -> dict: 1665 """ 1666 Get portfolio: all open positions, orders and some statistics for current `accountId`. 1667 If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile` 1668 and `overviewBondsCalendarFile` are defined then also save information to file. 1669 1670 WARNING! It is not recommended to run this method too many times in a loop! The server receives 1671 many requests about the state of the portfolio, and then, based on the received data, a large number 1672 of calculation and statistics are collected. 1673 1674 :param show: if `False` then only dictionary returns, if `True` then show more debug information. 1675 :param details: how detailed should the information be? 1676 - `full` — shows full available information about portfolio status (by default), 1677 - `positions` — shows only open positions, 1678 - `orders` — shows only sections of open limits and stop orders. 1679 - `digest` — show a short digest of the portfolio status, 1680 - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories, 1681 - `calendar` — shows only the bonds calendar section (if these present in portfolio). 1682 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 1683 :return: dictionary with client's raw portfolio and some statistics. 1684 """ 1685 if self.accountId is None or not self.accountId: 1686 uLogger.error("Variable `accountId` must be defined for using this method!") 1687 raise Exception("Account ID required") 1688 1689 view = { 1690 "raw": { # --- raw portfolio responses from broker with user portfolio data: 1691 "headers": {}, # list of dictionaries, response headers without "positions" section 1692 "Currencies": [], # list of dictionaries, open trades with currencies from "positions" section 1693 "Shares": [], # list of dictionaries, open trades with shares from "positions" section 1694 "Bonds": [], # list of dictionaries, open trades with bonds from "positions" section 1695 "Etfs": [], # list of dictionaries, open trades with etfs from "positions" section 1696 "Futures": [], # list of dictionaries, open trades with futures from "positions" section 1697 "positions": {}, # raw response from broker: dictionary with current available or blocked currencies and instruments for client 1698 "orders": [], # raw response from broker: list of dictionaries with all pending (market) orders 1699 "stopOrders": [], # raw response from broker: list of dictionaries with all stop orders 1700 "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}}, # dict with prices of all currencies in RUB 1701 }, 1702 "stat": { # --- some statistics calculated using "raw" sections: 1703 "portfolioCostRUB": 0., # portfolio cost in RUB (Russian Rouble) 1704 "availableRUB": 0., # available rubles (without other currencies) 1705 "blockedRUB": 0., # blocked sum in Russian Rouble 1706 "totalChangesRUB": 0., # changes for all open trades in RUB 1707 "totalChangesPercentRUB": 0., # changes for all open trades in percents 1708 "allCurrenciesCostRUB": 0., # costs of all currencies (include rubles) in RUB 1709 "sharesCostRUB": 0., # costs of all shares in RUB 1710 "bondsCostRUB": 0., # costs of all bonds in RUB 1711 "etfsCostRUB": 0., # costs of all etfs in RUB 1712 "futuresCostRUB": 0., # costs of all futures in RUB 1713 "Currencies": [], # list of dictionaries of all currencies statistics 1714 "Shares": [], # list of dictionaries of all shares statistics 1715 "Bonds": [], # list of dictionaries of all bonds statistics 1716 "Etfs": [], # list of dictionaries of all etfs statistics 1717 "Futures": [], # list of dictionaries of all futures statistics 1718 "orders": [], # list of dictionaries of all pending (market) orders and it's parameters 1719 "stopOrders": [], # list of dictionaries of all stop orders and it's parameters 1720 "blockedCurrencies": {}, # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21} 1721 "blockedInstruments": {}, # dict with blocked by FIGI, e.g. {} 1722 "funds": {}, # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1723 }, 1724 "analytics": { # --- some analytics of portfolio: 1725 "distrByAssets": {}, # portfolio distribution by assets 1726 "distrByCompanies": {}, # portfolio distribution by companies 1727 "distrBySectors": {}, # portfolio distribution by sectors 1728 "distrByCurrencies": {}, # portfolio distribution by currencies 1729 "distrByCountries": {}, # portfolio distribution by countries 1730 "bondsCalendar": None, # bonds payment calendar as Pandas DataFrame (if these present in portfolio) 1731 } 1732 } 1733 1734 details = details.lower() 1735 availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"] 1736 if details not in availableDetails: 1737 details = "full" 1738 uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails)) 1739 1740 uLogger.debug("Requesting portfolio of a client. Wait, please...") 1741 1742 portfolioResponse = self.RequestPortfolio() # current user's portfolio (dict) 1743 view["raw"]["positions"] = self.RequestPositions() # current open positions by instruments (dict) 1744 view["raw"]["orders"] = self.RequestPendingOrders() # current actual pending limit orders (list) 1745 view["raw"]["stopOrders"] = self.RequestStopOrders() # current actual stop orders (list) 1746 1747 # save response headers without "positions" section: 1748 for key in portfolioResponse.keys(): 1749 if key != "positions": 1750 view["raw"]["headers"][key] = portfolioResponse[key] 1751 1752 else: 1753 continue 1754 1755 # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation 1756 # Type of instrument must be only one of supported types in TKS_INSTRUMENTS 1757 for item in portfolioResponse["positions"]: 1758 if item["instrumentType"] == "currency": 1759 self._figi = item["figi"] 1760 if not self._figi and item["ticker"]: 1761 self._ticker = item["ticker"] 1762 self._figi = self.SearchByTicker()["figi"] # Get FIGI to avoid warnings 1763 1764 curr = self.SearchByFIGI(requestPrice=False) 1765 1766 # current price of currency in RUB: 1767 view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = { 1768 "name": curr["name"], 1769 "currentPrice": NanoToFloat( 1770 item["currentPrice"]["units"], 1771 item["currentPrice"]["nano"] 1772 ), 1773 } 1774 1775 view["raw"]["Currencies"].append(item) 1776 1777 elif item["instrumentType"] == "share": 1778 view["raw"]["Shares"].append(item) 1779 1780 elif item["instrumentType"] == "bond": 1781 view["raw"]["Bonds"].append(item) 1782 1783 elif item["instrumentType"] == "etf": 1784 view["raw"]["Etfs"].append(item) 1785 1786 elif item["instrumentType"] == "futures": 1787 view["raw"]["Futures"].append(item) 1788 1789 else: 1790 continue 1791 1792 # how many volume of currencies (by ISO currency name) are blocked: 1793 for item in view["raw"]["positions"]["blocked"]: 1794 blocked = NanoToFloat(item["units"], item["nano"]) 1795 if blocked > 0: 1796 view["stat"]["blockedCurrencies"][item["currency"]] = blocked 1797 1798 # how many volume of instruments (by FIGI) are blocked: 1799 for item in view["raw"]["positions"]["securities"]: 1800 blocked = int(item["blocked"]) 1801 if blocked > 0: 1802 view["stat"]["blockedInstruments"][item["figi"]] = blocked 1803 1804 allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]} 1805 1806 if "rub" in allBlocked.keys(): 1807 view["stat"]["blockedRUB"] = allBlocked["rub"] # blocked rubles 1808 1809 # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies: 1810 view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"]) 1811 view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"]) 1812 view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"]) 1813 view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"]) 1814 view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"]) 1815 view["stat"]["portfolioCostRUB"] = sum([ 1816 view["stat"]["allCurrenciesCostRUB"], 1817 view["stat"]["sharesCostRUB"], 1818 view["stat"]["bondsCostRUB"], 1819 view["stat"]["etfsCostRUB"], 1820 view["stat"]["futuresCostRUB"], 1821 ]) 1822 1823 # --- calculating some portfolio statistics: 1824 byComp = {} # distribution by companies 1825 bySect = {} # distribution by sectors 1826 byCurr = {} # distribution by currencies (include RUB) 1827 unknownCountryName = "All other countries" # default name for instruments without "countryOfRisk" and "countryOfRiskName" 1828 byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}} # distribution by countries (currencies are included in their countries) 1829 1830 for item in portfolioResponse["positions"]: 1831 self._figi = item["figi"] 1832 if not self._figi and item["ticker"]: 1833 self._ticker = item["ticker"] 1834 self._figi = self.SearchByTicker()["figi"] # Get FIGI to avoid warnings 1835 1836 instrument = self.SearchByFIGI(requestPrice=False) # full raw info about instrument by FIGI 1837 1838 if instrument: 1839 if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys(): 1840 blocked = allBlocked[instrument["nominal"]["currency"]] # blocked volume of currency 1841 1842 elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys(): 1843 blocked = allBlocked[item["figi"]] # blocked volume of other instruments 1844 1845 else: 1846 blocked = 0 1847 1848 volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"]) # available volume of instrument 1849 lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"]) # available volume in lots of instrument 1850 direction = "Long" if lots >= 0 else "Short" # direction of an instrument's position: short or long 1851 curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"]) # current instrument's price 1852 average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"]) # current average position price 1853 profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"]) # expected profit at current moment 1854 currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"] # currency name rub, usd, eur etc. 1855 cost = curPrice if "currentNkd" not in item.keys() else (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume # current cost of all volume of instrument in basic asset 1856 baseCurrencyName = item["currentPrice"]["currency"] # name of base currency (rub) 1857 countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName 1858 costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"] # cost in rubles 1859 percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0. # instrument's part in percent of full portfolio cost 1860 1861 statData = { 1862 "figi": item["figi"], # FIGI from REST API "GetPortfolio" method 1863 "ticker": instrument["ticker"], # ticker by FIGI 1864 "currency": currency, # currency name rub, usd, eur etc. for instrument price 1865 "volume": volume, # available volume of instrument 1866 "lots": lots, # volume in lots of instrument 1867 "direction": direction, # direction of an instrument's position: short or long 1868 "blocked": blocked, # blocked volume of currency or instrument 1869 "currentPrice": curPrice, # current instrument's price in basic asset 1870 "average": average, # current average position price 1871 "cost": cost, # current cost of all volume of instrument in basic asset 1872 "baseCurrencyName": baseCurrencyName, # name of base currency (rub) 1873 "costRUB": costRUB, # cost of instrument in ruble 1874 "percentCostRUB": percentCostRUB, # instrument's part in percent of full portfolio cost in RUB 1875 "profit": profit, # expected profit at current moment 1876 "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0, # expected percents of profit at current moment for this instrument 1877 "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other", 1878 "name": instrument["name"] if "name" in instrument.keys() else "", # human-readable names of instruments 1879 "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "", # ISO name for currencies only 1880 "country": countryName, # e.g. "[RU] Российская Федерация" or unknownCountryName 1881 "step": instrument["step"], # minimum price increment 1882 } 1883 1884 # adding distribution by unique countries: 1885 if statData["country"] not in byCountry.keys(): 1886 byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB} 1887 1888 else: 1889 byCountry[statData["country"]]["cost"] += costRUB 1890 byCountry[statData["country"]]["percent"] += percentCostRUB 1891 1892 if item["instrumentType"] != "currency": 1893 # adding distribution by unique companies: 1894 if statData["name"]: 1895 if statData["name"] not in byComp.keys(): 1896 byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB} 1897 1898 else: 1899 byComp[statData["name"]]["cost"] += costRUB 1900 byComp[statData["name"]]["percent"] += percentCostRUB 1901 1902 # adding distribution by unique sectors: 1903 if statData["sector"] not in bySect.keys(): 1904 bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB} 1905 1906 else: 1907 bySect[statData["sector"]]["cost"] += costRUB 1908 bySect[statData["sector"]]["percent"] += percentCostRUB 1909 1910 # adding distribution by unique currencies: 1911 if currency not in byCurr.keys(): 1912 byCurr[currency] = { 1913 "name": view["raw"]["currenciesCurrentPrices"][currency]["name"], 1914 "cost": costRUB, 1915 "percent": percentCostRUB 1916 } 1917 1918 else: 1919 byCurr[currency]["cost"] += costRUB 1920 byCurr[currency]["percent"] += percentCostRUB 1921 1922 # saving statistics for every instrument: 1923 if item["instrumentType"] == "currency": 1924 view["stat"]["Currencies"].append(statData) 1925 1926 # update dict with free funds for trading (total - blocked) by currencies 1927 # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1928 view["stat"]["funds"][currency] = { 1929 "total": volume, 1930 "totalCostRUB": costRUB, # total volume cost in rubles 1931 "free": volume - blocked, 1932 "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0, # free volume cost in rubles 1933 } 1934 1935 elif item["instrumentType"] == "share": 1936 view["stat"]["Shares"].append(statData) 1937 1938 elif item["instrumentType"] == "bond": 1939 view["stat"]["Bonds"].append(statData) 1940 1941 elif item["instrumentType"] == "etf": 1942 view["stat"]["Etfs"].append(statData) 1943 1944 elif item["instrumentType"] == "Futures": 1945 view["stat"]["Futures"].append(statData) 1946 1947 else: 1948 continue 1949 1950 # total changes in Russian Ruble: 1951 view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]]) # available RUB without other currencies 1952 view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0. 1953 startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100) 1954 view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost 1955 view["stat"]["funds"]["rub"] = { 1956 "total": view["stat"]["availableRUB"], 1957 "totalCostRUB": view["stat"]["availableRUB"], 1958 "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1959 "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1960 } 1961 1962 # --- pending limit orders sector data: 1963 uniquePendingOrdersFIGIs = [] # unique FIGIs of pending limit orders to avoid many times price requests 1964 uniquePendingOrders = {} # unique instruments with FIGIs as dictionary keys 1965 1966 for item in view["raw"]["orders"]: 1967 self._figi = item["figi"] 1968 1969 if item["figi"] not in uniquePendingOrdersFIGIs: 1970 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1971 1972 uniquePendingOrdersFIGIs.append(item["figi"]) 1973 uniquePendingOrders[item["figi"]] = instrument 1974 1975 else: 1976 instrument = uniquePendingOrders[item["figi"]] 1977 1978 if instrument: 1979 action = TKS_ORDER_DIRECTIONS[item["direction"]] 1980 orderType = TKS_ORDER_TYPES[item["orderType"]] 1981 orderState = TKS_ORDER_STATES[item["executionReportStatus"]] 1982 orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1983 1984 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1985 if item["direction"] == "ORDER_DIRECTION_BUY": 1986 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1987 1988 else: 1989 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1990 1991 # requested price for order execution: 1992 target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"]) 1993 1994 # necessary changes in percent to reach target from current price: 1995 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1996 1997 view["stat"]["orders"].append({ 1998 "orderID": item["orderId"], # orderId number parameter of current order 1999 "figi": item["figi"], # FIGI identification 2000 "ticker": instrument["ticker"], # ticker name by FIGI 2001 "lotsRequested": item["lotsRequested"], # requested lots value 2002 "lotsExecuted": item["lotsExecuted"], # how many lots are executed 2003 "currentPrice": lastPrice, # current instrument's price for defined action 2004 "targetPrice": target, # requested price for order execution in base currency 2005 "baseCurrencyName": item["initialSecurityPrice"]["currency"], # name of base currency 2006 "percentChanges": changes, # changes in percent to target from current price 2007 "currency": item["currency"], # instrument's currency name 2008 "action": action, # sell / buy / Unknown from TKS_ORDER_DIRECTIONS 2009 "type": orderType, # type of order from TKS_ORDER_TYPES 2010 "status": orderState, # order status from TKS_ORDER_STATES 2011 "date": orderDate, # string with order date and time from UTC format (without nano seconds part) 2012 }) 2013 2014 # --- stop orders sector data: 2015 uniqueStopOrdersFIGIs = [] # unique FIGIs of stop orders to avoid many times price requests 2016 uniqueStopOrders = {} # unique instruments with FIGIs as dictionary keys 2017 2018 for item in view["raw"]["stopOrders"]: 2019 self._figi = item["figi"] 2020 2021 if item["figi"] not in uniqueStopOrdersFIGIs: 2022 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 2023 2024 uniqueStopOrdersFIGIs.append(item["figi"]) 2025 uniqueStopOrders[item["figi"]] = instrument 2026 2027 else: 2028 instrument = uniqueStopOrders[item["figi"]] 2029 2030 if instrument: 2031 action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]] 2032 orderType = TKS_STOP_ORDER_TYPES[item["orderType"]] 2033 createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 2034 2035 # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order 2036 if "expirationTime" in item.keys(): 2037 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"] 2038 expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0] 2039 2040 else: 2041 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"] 2042 expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"] 2043 2044 # current instrument's price (last sellers order if buy, and last buyers order if sell): 2045 if item["direction"] == "STOP_ORDER_DIRECTION_BUY": 2046 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 2047 2048 else: 2049 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 2050 2051 # requested price when stop-order executed: 2052 target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"]) 2053 2054 # price for limit-order, set up when stop-order executed: 2055 limit = NanoToFloat(item["price"]["units"], item["price"]["nano"]) 2056 2057 # necessary changes in percent to reach target from current price: 2058 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 2059 2060 view["stat"]["stopOrders"].append({ 2061 "orderID": item["stopOrderId"], # stopOrderId number parameter of current stop-order 2062 "figi": item["figi"], # FIGI identification 2063 "ticker": instrument["ticker"], # ticker name by FIGI 2064 "lotsRequested": item["lotsRequested"], # requested lots value 2065 "currentPrice": lastPrice, # current instrument's price for defined action 2066 "targetPrice": target, # requested price for stop-order execution in base currency 2067 "limitPrice": limit, # price for limit-order, set up when stop-order executed, 0 if market order 2068 "baseCurrencyName": item["stopPrice"]["currency"], # name of base currency 2069 "percentChanges": changes, # changes in percent to target from current price 2070 "currency": item["currency"], # instrument's currency name 2071 "action": action, # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS 2072 "type": orderType, # type of order from TKS_STOP_ORDER_TYPES 2073 "expType": expType, # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES 2074 "createDate": createDate, # string with created order date and time from UTC format (without nano seconds part) 2075 "expDate": expDate, # string with expiration order date and time from UTC format (without nano seconds part) 2076 }) 2077 2078 # --- calculating data for analytics section: 2079 # portfolio distribution by assets: 2080 view["analytics"]["distrByAssets"] = { 2081 "Ruble": { 2082 "uniques": 1, 2083 "cost": view["stat"]["availableRUB"], 2084 "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2085 }, 2086 "Currencies": { 2087 "uniques": len(view["stat"]["Currencies"]), # all foreign currencies without RUB 2088 "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"], 2089 "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2090 }, 2091 "Shares": { 2092 "uniques": len(view["stat"]["Shares"]), 2093 "cost": view["stat"]["sharesCostRUB"], 2094 "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2095 }, 2096 "Bonds": { 2097 "uniques": len(view["stat"]["Bonds"]), 2098 "cost": view["stat"]["bondsCostRUB"], 2099 "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2100 }, 2101 "Etfs": { 2102 "uniques": len(view["stat"]["Etfs"]), 2103 "cost": view["stat"]["etfsCostRUB"], 2104 "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2105 }, 2106 "Futures": { 2107 "uniques": len(view["stat"]["Futures"]), 2108 "cost": view["stat"]["futuresCostRUB"], 2109 "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2110 }, 2111 } 2112 2113 # portfolio distribution by companies: 2114 view["analytics"]["distrByCompanies"]["All money cash"] = { 2115 "ticker": "", 2116 "cost": view["stat"]["allCurrenciesCostRUB"], 2117 "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2118 } 2119 view["analytics"]["distrByCompanies"].update(byComp) 2120 2121 # portfolio distribution by sectors: 2122 view["analytics"]["distrBySectors"]["All money cash"] = { 2123 "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"], 2124 "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"], 2125 } 2126 view["analytics"]["distrBySectors"].update(bySect) 2127 2128 # portfolio distribution by currencies: 2129 if "rub" not in view["analytics"]["distrByCurrencies"].keys(): 2130 view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0} 2131 2132 if self.moreDebug: 2133 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!") 2134 2135 view["analytics"]["distrByCurrencies"].update(byCurr) 2136 view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2137 view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2138 2139 # portfolio distribution by countries: 2140 if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys(): 2141 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0} 2142 2143 if self.moreDebug: 2144 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!") 2145 2146 view["analytics"]["distrByCountries"].update(byCountry) 2147 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2148 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2149 2150 # --- Prepare text statistics overview in human-readable: 2151 if show or onlyFiles: 2152 actualOnDate = datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) 2153 2154 # Whatever the value `details`, header not changes: 2155 info = [ 2156 "# Client's portfolio\n\n", 2157 "* **Actual on date:** [{} UTC]\n".format(actualOnDate), 2158 "* **Account ID:** [{}]\n".format(self.accountId), 2159 ] 2160 2161 if details in ["full", "positions", "digest"]: 2162 info.extend([ 2163 "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2164 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format( 2165 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2166 view["stat"]["totalChangesRUB"], 2167 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2168 view["stat"]["totalChangesPercentRUB"], 2169 ), 2170 ]) 2171 2172 if details in ["full", "positions"]: 2173 info.extend([ 2174 "## Open positions\n\n", 2175 "| Ticker [FIGI] | Volume (blocked) | Lots | Curr. price | Avg. price | Current volume cost | Profit (%) |\n", 2176 "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n", 2177 "| **Ruble:** | {:>31} | | | | | |\n".format( 2178 "{:.2f} ({:.2f}) rub".format( 2179 view["stat"]["availableRUB"], 2180 view["stat"]["blockedRUB"], 2181 ) 2182 ) 2183 ]) 2184 2185 def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list: 2186 return [ 2187 "| | | | | | | |\n", 2188 "| {:<27} | | | | | {:>19} | |\n".format( 2189 noTradeStr if noTradeStr else typeStr, 2190 "" if noTradeStr else "{:.2f} RUB".format(CostRUB), 2191 ), 2192 ] 2193 2194 def _InfoStr(data: dict, isCurr: bool = False) -> str: 2195 return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format( 2196 "{} [{}]".format(data["ticker"], data["figi"]), 2197 "{:.2f} ({:.2f}) {}".format( 2198 data["volume"], 2199 data["blocked"], 2200 data["currency"], 2201 ) if isCurr else "{:.0f} ({:.0f})".format( 2202 data["volume"], 2203 data["blocked"], 2204 ), 2205 "—" if isCurr else "{:.4f}".format(data["lots"]).rstrip("0").rstrip("."), 2206 "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a", 2207 "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a", 2208 "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]), 2209 "{}{:.2f} {} ({}{:.2f}%)".format( 2210 "+" if data["profit"] > 0 else "", 2211 data["profit"], data["baseCurrencyName"], 2212 "+" if data["percentProfit"] > 0 else "", 2213 data["percentProfit"], 2214 ), 2215 ) 2216 2217 # --- Show currencies section: 2218 if view["stat"]["Currencies"]: 2219 info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**")) 2220 for item in view["stat"]["Currencies"]: 2221 info.append(_InfoStr(item, isCurr=True)) 2222 2223 else: 2224 info.extend(_SplitStr(noTradeStr="**Currencies:** no trades")) 2225 2226 # --- Show shares section: 2227 if view["stat"]["Shares"]: 2228 info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**")) 2229 2230 for item in view["stat"]["Shares"]: 2231 info.append(_InfoStr(item)) 2232 2233 else: 2234 info.extend(_SplitStr(noTradeStr="**Shares:** no trades")) 2235 2236 # --- Show bonds section: 2237 if view["stat"]["Bonds"]: 2238 info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**")) 2239 2240 for item in view["stat"]["Bonds"]: 2241 info.append(_InfoStr(item)) 2242 2243 else: 2244 info.extend(_SplitStr(noTradeStr="**Bonds:** no trades")) 2245 2246 # --- Show etfs section: 2247 if view["stat"]["Etfs"]: 2248 info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**")) 2249 2250 for item in view["stat"]["Etfs"]: 2251 info.append(_InfoStr(item)) 2252 2253 else: 2254 info.extend(_SplitStr(noTradeStr="**Etfs:** no trades")) 2255 2256 # --- Show futures section: 2257 if view["stat"]["Futures"]: 2258 info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**")) 2259 2260 for item in view["stat"]["Futures"]: 2261 info.append(_InfoStr(item)) 2262 2263 else: 2264 info.extend(_SplitStr(noTradeStr="**Futures:** no trades")) 2265 2266 if details in ["full", "orders"]: 2267 # --- Show pending limit orders section: 2268 if view["stat"]["orders"]: 2269 info.extend([ 2270 "\n## Opened pending limit-orders: [{}]\n".format(len(view["stat"]["orders"])), 2271 "\n| Ticker [FIGI] | Order ID | Lots (exec.) | Current price (% delta) | Target price | Action | Type | Create date (UTC) |\n", 2272 "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n", 2273 ]) 2274 2275 for item in view["stat"]["orders"]: 2276 info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format( 2277 "{} [{}]".format(item["ticker"], item["figi"]), 2278 item["orderID"], 2279 "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]), 2280 "{} {} ({}{:.2f}%)".format( 2281 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2282 item["baseCurrencyName"], 2283 "+" if item["percentChanges"] > 0 else "", 2284 float(item["percentChanges"]), 2285 ), 2286 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2287 item["action"], 2288 item["type"], 2289 item["date"], 2290 )) 2291 2292 else: 2293 info.append("\n## Total pending limit-orders: [0]\n") 2294 2295 # --- Show stop orders section: 2296 if view["stat"]["stopOrders"]: 2297 info.extend([ 2298 "\n## Opened stop-orders: [{}]\n".format(len(view["stat"]["stopOrders"])), 2299 "\n| Ticker [FIGI] | Stop order ID | Lots | Current price (% delta) | Target price | Limit price | Action | Type | Expire type | Create date (UTC) | Expiration (UTC) |\n", 2300 "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n", 2301 ]) 2302 2303 for item in view["stat"]["stopOrders"]: 2304 info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format( 2305 "{} [{}]".format(item["ticker"], item["figi"]), 2306 item["orderID"], 2307 item["lotsRequested"], 2308 "{} {} ({}{:.2f}%)".format( 2309 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2310 item["baseCurrencyName"], 2311 "+" if item["percentChanges"] > 0 else "", 2312 float(item["percentChanges"]), 2313 ), 2314 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2315 "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"], 2316 item["action"], 2317 item["type"], 2318 item["expType"], 2319 item["createDate"], 2320 item["expDate"], 2321 )) 2322 2323 else: 2324 info.append("\n## Total stop-orders: [0]\n") 2325 2326 if details in ["full", "analytics"]: 2327 # -- Show analytics section: 2328 if view["stat"]["portfolioCostRUB"] > 0: 2329 info.extend([ 2330 "\n# Analytics\n\n" 2331 "* **Actual on date:** [{} UTC]\n".format(actualOnDate), 2332 "* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2333 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format( 2334 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2335 view["stat"]["totalChangesRUB"], 2336 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2337 view["stat"]["totalChangesPercentRUB"], 2338 ), 2339 "\n## Portfolio distribution by assets\n" 2340 "\n| Type | Uniques | Percent | Current cost |\n", 2341 "|------------------------------------|---------|---------|--------------------|\n", 2342 ]) 2343 2344 for key in view["analytics"]["distrByAssets"].keys(): 2345 if view["analytics"]["distrByAssets"][key]["cost"] > 0: 2346 info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format( 2347 key, 2348 view["analytics"]["distrByAssets"][key]["uniques"], 2349 "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]), 2350 "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]), 2351 )) 2352 2353 aSepLine = "|----------------------------------------------|---------|--------------------|\n" 2354 2355 info.extend([ 2356 "\n## Portfolio distribution by companies\n" 2357 "\n| Company | Percent | Current cost |\n", 2358 aSepLine, 2359 ]) 2360 2361 for company in view["analytics"]["distrByCompanies"].keys(): 2362 if view["analytics"]["distrByCompanies"][company]["cost"] > 0: 2363 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2364 "{}{}".format( 2365 "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "", 2366 company, 2367 ), 2368 "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]), 2369 "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]), 2370 )) 2371 2372 info.extend([ 2373 "\n## Portfolio distribution by sectors\n" 2374 "\n| Sector | Percent | Current cost |\n", 2375 aSepLine, 2376 ]) 2377 2378 for sector in view["analytics"]["distrBySectors"].keys(): 2379 if view["analytics"]["distrBySectors"][sector]["cost"] > 0: 2380 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2381 sector, 2382 "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]), 2383 "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]), 2384 )) 2385 2386 info.extend([ 2387 "\n## Portfolio distribution by currencies\n" 2388 "\n| Instruments currencies | Percent | Current cost |\n", 2389 aSepLine, 2390 ]) 2391 2392 for curr in view["analytics"]["distrByCurrencies"].keys(): 2393 if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0: 2394 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2395 "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]), 2396 "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]), 2397 "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]), 2398 )) 2399 2400 info.extend([ 2401 "\n## Portfolio distribution by countries\n" 2402 "\n| Assets by country | Percent | Current cost |\n", 2403 aSepLine, 2404 ]) 2405 2406 for country in view["analytics"]["distrByCountries"].keys(): 2407 if view["analytics"]["distrByCountries"][country]["cost"] > 0: 2408 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2409 country, 2410 "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]), 2411 "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]), 2412 )) 2413 2414 if details in ["full", "calendar"]: 2415 # -- Show bonds payment calendar section: 2416 if view["stat"]["Bonds"]: 2417 bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]] 2418 view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False) 2419 info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False)) 2420 2421 else: 2422 info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n") 2423 2424 infoText = "".join(info) 2425 2426 if show and not onlyFiles: 2427 uLogger.info(infoText) 2428 2429 if details == "full" and self.overviewFile: 2430 filename = self.overviewFile 2431 2432 elif details == "digest" and self.overviewDigestFile: 2433 filename = self.overviewDigestFile 2434 2435 elif details == "positions" and self.overviewPositionsFile: 2436 filename = self.overviewPositionsFile 2437 2438 elif details == "orders" and self.overviewOrdersFile: 2439 filename = self.overviewOrdersFile 2440 2441 elif details == "analytics" and self.overviewAnalyticsFile: 2442 filename = self.overviewAnalyticsFile 2443 2444 elif details == "calendar" and self.overviewBondsCalendarFile: 2445 filename = self.overviewBondsCalendarFile 2446 2447 else: 2448 filename = "" 2449 2450 if filename and (show or onlyFiles): 2451 with open(filename, "w", encoding="UTF-8") as fH: 2452 fH.write(infoText) 2453 2454 uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename))) 2455 2456 if self.useHTMLReports: 2457 htmlFilePath = filename.replace(".md", ".html") if filename.endswith(".md") else filename + ".html" 2458 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 2459 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's portfolio", commonCSS=COMMON_CSS, markdown=infoText)) 2460 2461 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 2462 2463 return view
Get portfolio: all open positions, orders and some statistics for current accountId.
If overviewFile, overviewDigestFile, overviewPositionsFile, overviewOrdersFile, overviewAnalyticsFile
and overviewBondsCalendarFile are defined then also save information to file.
WARNING! It is not recommended to run this method too many times in a loop! The server receives many requests about the state of the portfolio, and then, based on the received data, a large number of calculation and statistics are collected.
Parameters
- show: if
Falsethen only dictionary returns, ifTruethen show more debug information. - details: how detailed should the information be?
full— shows full available information about portfolio status (by default),positions— shows only open positions,orders— shows only sections of open limits and stop orders.digest— show a short digest of the portfolio status,analytics— shows only the analytics section and the distribution of the portfolio by various categories,calendar— shows only the bonds calendar section (if these present in portfolio).
- onlyFiles: if
Truethen do not show Markdown table in the console, but only generates report files.
Returns
dictionary with client's raw portfolio and some statistics.
2465 def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True, onlyFiles=False) -> tuple[list[dict], dict]: 2466 """ 2467 Returns history operations between two given dates for current `accountId`. 2468 If `reportFile` string is not empty then also save human-readable report. 2469 Shows some statistical data of closed positions. 2470 2471 :param start: see docstring in `TradeRoutines.GetDatesAsString()` method. 2472 :param end: see docstring in `TradeRoutines.GetDatesAsString()` method. 2473 :param show: if `True` then also prints all records to the console. 2474 :param showCancelled: if `False` then remove information about cancelled operations from the deals report. 2475 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 2476 :return: original list of dictionaries with history of deals records from API ("operations" key): 2477 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2478 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc. 2479 """ 2480 if self.accountId is None or not self.accountId: 2481 uLogger.error("Variable `accountId` must be defined for using this method!") 2482 raise Exception("Account ID required") 2483 2484 startDate, endDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT) # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2485 2486 uLogger.debug("Requesting history of a client's operations. Wait, please...") 2487 2488 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2489 dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations" 2490 self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate}) 2491 ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"] # list of dict: operations returns by broker 2492 customStat = {} # custom statistics in additional to responseJSON 2493 2494 # --- output report in human-readable format: 2495 if self.reportFile and (show or onlyFiles): 2496 splitLine1 = "| | | | | |\n" # Summary section 2497 splitLine2 = "| | | | | | | | |\n" # Operations section 2498 nextDay = "" 2499 2500 info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])] 2501 2502 if len(ops) > 0: 2503 customStat = { 2504 "opsCount": 0, # total operations count 2505 "buyCount": 0, # buy operations 2506 "sellCount": 0, # sell operations 2507 "buyTotal": {"rub": 0.}, # Buy sums in different currencies 2508 "sellTotal": {"rub": 0.}, # Sell sums in different currencies 2509 "payIn": {"rub": 0.}, # Deposit brokerage account 2510 "payOut": {"rub": 0.}, # Withdrawals 2511 "divs": {"rub": 0.}, # Dividends income 2512 "coupons": {"rub": 0.}, # Coupon's income 2513 "brokerCom": {"rub": 0.}, # Service commissions 2514 "serviceCom": {"rub": 0.}, # Service commissions 2515 "marginCom": {"rub": 0.}, # Margin commissions 2516 "allTaxes": {"rub": 0.}, # Sum of withholding taxes and corrections 2517 } 2518 2519 # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES: 2520 for item in ops: 2521 if item["state"] == "OPERATION_STATE_EXECUTED": 2522 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2523 2524 # count buy operations: 2525 if "_BUY" in item["operationType"]: 2526 customStat["buyCount"] += 1 2527 2528 if item["payment"]["currency"] in customStat["buyTotal"].keys(): 2529 customStat["buyTotal"][item["payment"]["currency"]] += payment 2530 2531 else: 2532 customStat["buyTotal"][item["payment"]["currency"]] = payment 2533 2534 # count sell operations: 2535 elif "_SELL" in item["operationType"]: 2536 customStat["sellCount"] += 1 2537 2538 if item["payment"]["currency"] in customStat["sellTotal"].keys(): 2539 customStat["sellTotal"][item["payment"]["currency"]] += payment 2540 2541 else: 2542 customStat["sellTotal"][item["payment"]["currency"]] = payment 2543 2544 # count incoming operations: 2545 elif item["operationType"] in ["OPERATION_TYPE_INPUT"]: 2546 if item["payment"]["currency"] in customStat["payIn"].keys(): 2547 customStat["payIn"][item["payment"]["currency"]] += payment 2548 2549 else: 2550 customStat["payIn"][item["payment"]["currency"]] = payment 2551 2552 # count withdrawals operations: 2553 elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]: 2554 if item["payment"]["currency"] in customStat["payOut"].keys(): 2555 customStat["payOut"][item["payment"]["currency"]] += payment 2556 2557 else: 2558 customStat["payOut"][item["payment"]["currency"]] = payment 2559 2560 # count dividends income: 2561 elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]: 2562 if item["payment"]["currency"] in customStat["divs"].keys(): 2563 customStat["divs"][item["payment"]["currency"]] += payment 2564 2565 else: 2566 customStat["divs"][item["payment"]["currency"]] = payment 2567 2568 # count coupon's income: 2569 elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]: 2570 if item["payment"]["currency"] in customStat["coupons"].keys(): 2571 customStat["coupons"][item["payment"]["currency"]] += payment 2572 2573 else: 2574 customStat["coupons"][item["payment"]["currency"]] = payment 2575 2576 # count broker commissions: 2577 elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]: 2578 if item["payment"]["currency"] in customStat["brokerCom"].keys(): 2579 customStat["brokerCom"][item["payment"]["currency"]] += payment 2580 2581 else: 2582 customStat["brokerCom"][item["payment"]["currency"]] = payment 2583 2584 # count service commissions: 2585 elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]: 2586 if item["payment"]["currency"] in customStat["serviceCom"].keys(): 2587 customStat["serviceCom"][item["payment"]["currency"]] += payment 2588 2589 else: 2590 customStat["serviceCom"][item["payment"]["currency"]] = payment 2591 2592 # count margin commissions: 2593 elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]: 2594 if item["payment"]["currency"] in customStat["marginCom"].keys(): 2595 customStat["marginCom"][item["payment"]["currency"]] += payment 2596 2597 else: 2598 customStat["marginCom"][item["payment"]["currency"]] = payment 2599 2600 # count withholding taxes: 2601 elif "_TAX" in item["operationType"]: 2602 if item["payment"]["currency"] in customStat["allTaxes"].keys(): 2603 customStat["allTaxes"][item["payment"]["currency"]] += payment 2604 2605 else: 2606 customStat["allTaxes"][item["payment"]["currency"]] = payment 2607 2608 else: 2609 continue 2610 2611 customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"] 2612 2613 # --- view "Actions" lines: 2614 info.extend([ 2615 "| Report sections | | | | |\n", 2616 "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n", 2617 "| **Actions:** | Trades: {:<21} | Trading volumes: | | |\n".format(customStat["opsCount"]), 2618 "| | Buy: {:<22} | {:<28} | | |\n".format( 2619 "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2620 " rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else " —", 2621 ), 2622 "| | Sell: {:<21} | {:<28} | | |\n".format( 2623 "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2624 " rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else " —", 2625 ), 2626 ]) 2627 2628 opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys())))) 2629 for key in opsKeys: 2630 if key == "rub": 2631 continue 2632 2633 info.extend([ 2634 "| | | {:<28} | | |\n".format( 2635 " {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0) 2636 ), 2637 "| | | {:<28} | | |\n".format( 2638 " {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0) 2639 ), 2640 ]) 2641 2642 info.append(splitLine1) 2643 2644 def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str: 2645 return "| | {:<29} | {:<28} | {:<20} | {:<22} |\n".format( 2646 " {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else " —", 2647 " {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else " —", 2648 " {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else " —", 2649 " {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else " —", 2650 ) 2651 2652 # --- view "Payments" lines: 2653 info.append("| **Payments:** | Deposit on broker account: | Withdrawals: | Dividends income: | Coupons income: |\n") 2654 paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys())))) 2655 2656 for key in paymentsKeys: 2657 info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key)) 2658 2659 info.append(splitLine1) 2660 2661 # --- view "Commissions and taxes" lines: 2662 info.append("| **Commissions and taxes:** | Broker commissions: | Service commissions: | Margin commissions: | All taxes/corrections: |\n") 2663 comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys())))) 2664 2665 for key in comKeys: 2666 info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key)) 2667 2668 info.extend([ 2669 "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"), 2670 "| Date and time | FIGI | Ticker | Asset | Value | Payment | Status | Operation type |\n", 2671 "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n", 2672 ]) 2673 2674 else: 2675 info.append("Broker returned no operations during this period\n") 2676 2677 # --- view "Operations" section: 2678 for item in ops: 2679 if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]: 2680 continue 2681 2682 else: 2683 self._figi = item["figi"] 2684 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2685 instrument = self.SearchByFIGI(requestPrice=False) if self._figi else {} 2686 2687 # group of deals during one day: 2688 if nextDay and item["date"].split("T")[0] != nextDay: 2689 info.append(splitLine2) 2690 nextDay = "" 2691 2692 else: 2693 nextDay = item["date"].split("T")[0] # saving current day for splitting 2694 2695 info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format( 2696 item["date"].replace("T", " ").replace("Z", "").split(".")[0], 2697 self._figi if self._figi else "—", 2698 instrument["ticker"] if instrument else "—", 2699 instrument["type"] if instrument else "—", 2700 item["quantity"] if int(item["quantity"]) > 0 else "—", 2701 "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—", 2702 TKS_OPERATION_STATES[item["state"]], 2703 TKS_OPERATION_TYPES[item["operationType"]], 2704 )) 2705 2706 infoText = "".join(info) 2707 2708 if show and not onlyFiles: 2709 if self.moreDebug: 2710 uLogger.debug("Records about history of a client's operations successfully received") 2711 2712 uLogger.info(infoText) 2713 2714 if self.reportFile and (show or onlyFiles): 2715 with open(self.reportFile, "w", encoding="UTF-8") as fH: 2716 fH.write(infoText) 2717 2718 uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile))) 2719 2720 if self.useHTMLReports: 2721 htmlFilePath = self.reportFile.replace(".md", ".html") if self.reportFile.endswith(".md") else self.reportFile + ".html" 2722 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 2723 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's operations", commonCSS=COMMON_CSS, markdown=infoText)) 2724 2725 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 2726 2727 return ops, customStat
Returns history operations between two given dates for current accountId.
If reportFile string is not empty then also save human-readable report.
Shows some statistical data of closed positions.
Parameters
- start: see docstring in
TradeRoutines.GetDatesAsString()method. - end: see docstring in
TradeRoutines.GetDatesAsString()method. - show: if
Truethen also prints all records to the console. - showCancelled: if
Falsethen remove information about cancelled operations from the deals report. - onlyFiles: if
Truethen do not show Markdown table in the console, but only generates report files.
Returns
original list of dictionaries with history of deals records from API ("operations" key): https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2729 def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False, onlyFiles=False) -> pd.DataFrame: 2730 """ 2731 This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id). 2732 2733 History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. 2734 Warning! Broker server used ISO UTC time by default. 2735 2736 If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame. 2737 Also, `historyFile` used to update history with `onlyMissing` parameter. 2738 2739 See also: `LoadHistory()` and `ShowHistoryChart()` methods. 2740 2741 :param start: see docstring in `TradeRoutines.GetDatesAsString()` method. 2742 :param end: see docstring in `TradeRoutines.GetDatesAsString()` method. 2743 :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`, 2744 `"hour"`, `"day"`. Default: `"hour"`. 2745 :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`. 2746 False by default. Warning! History appends only from last candle to current time 2747 with always update last candle! 2748 :param csvSep: separator if csv-file is used, `,` by default. 2749 :param show: if `True` then also prints Pandas DataFrame to the console. 2750 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 2751 :return: Pandas DataFrame with prices history. Headers of columns are defined by default: 2752 `["date", "time", "open", "high", "low", "close", "volume"]`. 2753 """ 2754 strStartDate, strEndDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT) # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2755 headers = ["date", "time", "open", "high", "low", "close", "volume"] # sequence and names of column headers 2756 history = None # empty pandas object for history 2757 2758 if interval not in TKS_CANDLE_INTERVALS.keys(): 2759 uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.") 2760 raise Exception("Incorrect value") 2761 2762 if not (self._ticker or self._figi): 2763 uLogger.error("Ticker or FIGI must be defined!") 2764 raise Exception("Ticker or FIGI required") 2765 2766 if self._ticker and not self._figi: 2767 instrumentByTicker = self.SearchByTicker(requestPrice=False) 2768 self._figi = instrumentByTicker["figi"] if instrumentByTicker else "" 2769 2770 if self._figi and not self._ticker: 2771 instrumentByFIGI = self.SearchByFIGI(requestPrice=False) 2772 self._ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else "" 2773 2774 dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from start time string 2775 dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from end time string 2776 if interval.lower() != "day": 2777 dtEnd += timedelta(seconds=1) # adds 1 sec for requests, because day end returned by `TradeRoutines.GetDatesAsString()` is 23:59:59 2778 2779 delta = dtEnd - dtStart # current UTC time minus last time in file 2780 deltaMinutes = delta.days * 1440 + delta.seconds // 60 # minutes between start and end dates 2781 2782 # calculate history length in candles: 2783 length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1] 2784 if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0: 2785 length += 1 # to avoid fraction time 2786 2787 # calculate data blocks count: 2788 blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2] 2789 2790 uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self._ticker, self._figi)) 2791 if self.moreDebug: 2792 uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end)) 2793 uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate)) 2794 uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval)) 2795 uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2])) 2796 2797 tempOld = None # pandas object for old history, if --only-missing key present 2798 lastTime = None # datetime object of last old candle in file 2799 2800 if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile): 2801 if self.moreDebug: 2802 uLogger.debug("--only-missing key present, add only last missing candles...") 2803 uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile))) 2804 2805 tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers) 2806 2807 tempOld["date"] = pd.to_datetime(tempOld["date"]) # load date "as is" 2808 tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d") # convert date to string 2809 tempOld["time"] = pd.to_datetime(tempOld["time"]) # load time "as is" 2810 tempOld["time"] = tempOld["time"].dt.strftime("%H:%M") # convert time to string 2811 2812 # get last datetime object from last string in file or minus 1 delta if file is empty: 2813 if len(tempOld) > 0: 2814 lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2815 2816 else: 2817 lastTime = dtEnd - timedelta(days=1) # history file is empty, so last date set at -1 day 2818 2819 tempOld = tempOld[:-1] # always remove last old candle because it may be incompletely at the current time 2820 2821 responseJSONs = [] # raw history blocks of data 2822 2823 blockEnd = dtEnd 2824 for item in range(blocks): 2825 tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2] 2826 blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail) 2827 2828 uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format( 2829 item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2830 )) 2831 2832 if blockStart == blockEnd: 2833 uLogger.debug("Skipped this zero-length block...") 2834 2835 else: 2836 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles 2837 historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles" 2838 self.body = str({ 2839 "figi": self._figi, 2840 "from": blockStart.strftime(TKS_DATE_TIME_FORMAT), 2841 "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2842 "interval": TKS_CANDLE_INTERVALS[interval][0] 2843 }) 2844 responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1) 2845 2846 if "code" in responseJSON.keys(): 2847 uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks)) 2848 2849 else: 2850 if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1: 2851 responseJSON["candles"] = responseJSON["candles"][:-1] # removes last candle for "yesterday" request 2852 2853 responseJSONs = responseJSON["candles"] + responseJSONs # add more old history behind newest dates 2854 2855 blockEnd = blockStart 2856 2857 printCount = len(responseJSONs) # candles to show in console 2858 if responseJSONs: 2859 tempHistory = pd.DataFrame( 2860 data={ 2861 "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2862 "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2863 "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs], 2864 "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs], 2865 "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs], 2866 "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs], 2867 "volume": [int(item["volume"]) for item in responseJSONs], 2868 }, 2869 index=range(len(responseJSONs)), 2870 columns=["date", "time", "open", "high", "low", "close", "volume"], 2871 ) 2872 tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d") 2873 tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M") 2874 2875 # append only newest candles to old history if --only-missing key present: 2876 if onlyMissing and tempOld is not None and lastTime is not None: 2877 index = 0 # find start index in tempHistory data: 2878 2879 for i, item in tempHistory.iterrows(): 2880 curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2881 2882 if curTime == lastTime: 2883 uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 2884 index = i 2885 printCount = index + 1 2886 break 2887 2888 history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True) 2889 2890 else: 2891 history = tempHistory # if no `--only-missing` key then load full data from server 2892 2893 if self.moreDebug: 2894 uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False))) 2895 2896 if history is not None and not history.empty: 2897 if show and not onlyFiles: 2898 uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format( 2899 strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]), 2900 pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False), 2901 )) 2902 2903 else: 2904 uLogger.warning("Received an empty candles history!") 2905 2906 if self.historyFile is not None: 2907 if history is not None and not history.empty: 2908 history.to_csv(self.historyFile, sep=csvSep, index=False, header=False) 2909 uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self._ticker, self._figi, interval, os.path.abspath(self.historyFile))) 2910 2911 else: 2912 uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile))) 2913 2914 else: 2915 if self.moreDebug: 2916 uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.") 2917 2918 return history
This method returns last history candles of the current instrument defined by ticker or figi (FIGI id).
History returned between two given dates: start and end. Minimum requested date in the past is 1970-01-01.
Warning! Broker server used ISO UTC time by default.
If historyFile is not None then method save history to file, otherwise return only Pandas DataFrame.
Also, historyFile used to update history with onlyMissing parameter.
See also: LoadHistory() and ShowHistoryChart() methods.
Parameters
- start: see docstring in
TradeRoutines.GetDatesAsString()method. - end: see docstring in
TradeRoutines.GetDatesAsString()method. - interval: this is a candle interval. Current available values are
"1min","5min","15min","hour","day". Default:"hour". - onlyMissing: if
Truethen add only last missing candles, do not request all history length fromstart. False by default. Warning! History appends only from last candle to current time with always update last candle! - csvSep: separator if csv-file is used,
,by default. - show: if
Truethen also prints Pandas DataFrame to the console. - onlyFiles: if
Truethen do not show Markdown table in the console, but only generates report files.
Returns
Pandas DataFrame with prices history. Headers of columns are defined by default:
["date", "time", "open", "high", "low", "close", "volume"].
2920 def LoadHistory(self, filePath: str) -> pd.DataFrame: 2921 """ 2922 Load candles history from csv-file and return Pandas DataFrame object. 2923 2924 See also: `History()` and `ShowHistoryChart()` methods. 2925 2926 :param filePath: path to csv-file to open. 2927 """ 2928 loadedHistory = None # init candles data object 2929 2930 uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...") 2931 2932 if os.path.exists(filePath): 2933 loadedHistory = self.priceModel.LoadFromFile(filePath) # load data and get chain of candles as Pandas DataFrame 2934 2935 tfStr = self.priceModel.FormattedDelta( 2936 self.priceModel.timeframe, 2937 "{days} days {hours}h {minutes}m {seconds}s", 2938 ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta( 2939 self.priceModel.timeframe, 2940 "{hours}h {minutes}m {seconds}s", 2941 ) 2942 2943 if loadedHistory is not None and not loadedHistory.empty: 2944 uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format( 2945 len(loadedHistory), 2946 tfStr, 2947 pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)), 2948 ) 2949 2950 else: 2951 uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath))) 2952 2953 else: 2954 uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath)) 2955 2956 return loadedHistory
Load candles history from csv-file and return Pandas DataFrame object.
See also: History() and ShowHistoryChart() methods.
Parameters
- filePath: path to csv-file to open.
2958 def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None: 2959 """ 2960 Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file. 2961 2962 Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart. 2963 Default: `index.html` (both for interact and non-interact candlesticks chart). 2964 2965 See also: `History()` and `LoadHistory()` methods. 2966 2967 :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object. 2968 :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. 2969 See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters 2970 If False then chain of candlesticks will render as not interactive Google Candlestick chart. 2971 See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template 2972 :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to 2973 html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file. 2974 """ 2975 if isinstance(candles, str): 2976 self.priceModel.prices = self.LoadHistory(filePath=candles) # load candles chain from file 2977 self.priceModel.ticker = os.path.basename(candles) # use filename as ticker name in PriceGenerator 2978 2979 elif isinstance(candles, pd.DataFrame): 2980 self.priceModel.prices = candles # set candles chain from variable 2981 self.priceModel.ticker = self._ticker # use current TKSBrokerAPI ticker as ticker name in PriceGenerator 2982 2983 if "datetime" not in candles.columns: 2984 self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True) # PriceGenerator uses "datetime" column with date and time 2985 2986 else: 2987 uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!") 2988 raise Exception("Incorrect value") 2989 2990 self.priceModel.horizon = len(self.priceModel.prices) # use length of candles data as horizon in PriceGenerator 2991 2992 if interact: 2993 uLogger.debug("Rendering interactive candles chart. Wait, please...") 2994 2995 self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2996 2997 else: 2998 uLogger.debug("Rendering non-interactive candles chart. Wait, please...") 2999 3000 self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 3001 3002 uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))
Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
Self variable htmlHistoryFile can be use as html-file name to save interaction or non-interaction chart.
Default: index.html (both for interact and non-interact candlesticks chart).
See also: History() and LoadHistory() methods.
Parameters
- candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
- interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters If False then chain of candlesticks will render as not interactive Google Candlestick chart. See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
- openInBrowser: if True then immediately open chart in default browser, otherwise only path to
html-file prints to console. False by default, to avoid issues with
permissions deniedto html-file.
3004 def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 3005 """ 3006 Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response. 3007 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 3008 3009 See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`. 3010 3011 :param operation: string "Buy" or "Sell". 3012 :param lots: volume, integer count of lots >= 1. 3013 :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`. 3014 :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`. 3015 :param expDate: string "Undefined" by default or local date in future, 3016 it is a string with format `%Y-%m-%d %H:%M:%S`. 3017 :return: JSON with response from broker server. 3018 """ 3019 if self.accountId is None or not self.accountId: 3020 uLogger.error("Variable `accountId` must be defined for using this method!") 3021 raise Exception("Account ID required") 3022 3023 if operation is None or not operation or operation not in ("Buy", "Sell"): 3024 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 3025 raise Exception("Incorrect value") 3026 3027 if lots is None or lots < 1: 3028 uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.") 3029 lots = 1 3030 3031 if tp is None or tp < 0: 3032 tp = 0 3033 3034 if sl is None or sl < 0: 3035 sl = 0 3036 3037 if expDate is None or not expDate: 3038 expDate = "Undefined" 3039 3040 if not (self._ticker or self._figi): 3041 uLogger.error("Ticker or FIGI must be defined!") 3042 raise Exception("Ticker or FIGI required") 3043 3044 instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True) 3045 self._ticker = instrument["ticker"] 3046 self._figi = instrument["figi"] 3047 3048 uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self._ticker, self._figi, lots, tp, sl, expDate)) 3049 3050 openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3051 self.body = str({ 3052 "figi": self._figi, 3053 "quantity": str(lots), 3054 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3055 "accountId": str(self.accountId), 3056 "orderType": "ORDER_TYPE_MARKET", # see: TKS_ORDER_TYPES 3057 }) 3058 response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0) 3059 3060 if "orderId" in response.keys(): 3061 uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format( 3062 operation, response["orderId"], 3063 self._ticker, self._figi, lots, 3064 NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"], 3065 NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"], 3066 NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"], 3067 )) 3068 3069 if tp > 0: 3070 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate) 3071 3072 if sl > 0: 3073 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate) 3074 3075 else: 3076 uLogger.warning("Not `oK` status received! Market order not executed. See full debug log and try again open order later.") 3077 3078 return response
Universal method to create market order and make deal at the current price for current accountId. Returns JSON data with response.
If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.
See also: Order() docstring. More simple methods than Trade() are Buy() and Sell().
Parameters
- operation: string "Buy" or "Sell".
- lots: volume, integer count of lots >= 1.
- tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter
targetPriceinself.Order(). - sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter
targetPriceinself.Order(). - expDate: string "Undefined" by default or local date in future,
it is a string with format
%Y-%m-%d %H:%M:%S.
Returns
JSON with response from broker server.
3080 def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 3081 """ 3082 More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response. 3083 If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter. 3084 3085 See also: `Order()` and `Trade()` docstrings. 3086 3087 :param lots: volume, integer count of lots >= 1. 3088 :param tp: float > 0, take profit price of stop-order. 3089 :param sl: float > 0, stop loss price of stop-order. 3090 :param expDate: it's a local date in future. 3091 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3092 :return: JSON with response from broker server. 3093 """ 3094 return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)
More simple method than Trade(). Create Buy market order and make deal at the current price. Returns JSON data with response.
If tp or sl > 0, then in additional will opens stop-orders with "TP" and "SL" flags for stopType parameter.
See also: Order() and Trade() docstrings.
Parameters
- lots: volume, integer count of lots >= 1.
- tp: float > 0, take profit price of stop-order.
- sl: float > 0, stop loss price of stop-order.
- expDate: it's a local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S.
Returns
JSON with response from broker server.
3096 def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 3097 """ 3098 More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response. 3099 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 3100 3101 See also: `Order()` and `Trade()` docstrings. 3102 3103 :param lots: volume, integer count of lots >= 1. 3104 :param tp: float > 0, take profit price of stop-order. 3105 :param sl: float > 0, stop loss price of stop-order. 3106 :param expDate: it's a local date in the future. 3107 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3108 :return: JSON with response from broker server. 3109 """ 3110 return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)
More simple method than Trade(). Create Sell market order and make deal at the current price. Returns JSON data with response.
If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.
See also: Order() and Trade() docstrings.
Parameters
- lots: volume, integer count of lots >= 1.
- tp: float > 0, take profit price of stop-order.
- sl: float > 0, stop loss price of stop-order.
- expDate: it's a local date in the future.
String has a format like this:
%Y-%m-%d %H:%M:%S.
Returns
JSON with response from broker server.
3112 def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None: 3113 """ 3114 Close position of given instruments. 3115 3116 :param instruments: list of instruments defined by tickers or FIGIs that must be closed. 3117 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3118 This avoids unnecessary downloading data from the server. 3119 """ 3120 if instruments is None or not instruments: 3121 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 3122 raise Exception("Ticker or FIGI required") 3123 3124 if isinstance(instruments, str): 3125 instruments = [instruments] 3126 3127 uniqueInstruments = self.GetUniqueFIGIs(instruments) 3128 if uniqueInstruments: 3129 if portfolio is None or not portfolio: 3130 portfolio = self.Overview(show=False) 3131 3132 allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]] 3133 uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened))) 3134 3135 for self._figi in uniqueInstruments: 3136 if self._figi not in allOpened: 3137 uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self._figi)) 3138 continue 3139 3140 # search open trade info about instrument by ticker: 3141 instrument = {} 3142 for iType in TKS_INSTRUMENTS: 3143 if instrument: 3144 break 3145 3146 for item in portfolio["stat"][iType]: 3147 if item["figi"] == self._figi: 3148 instrument = item 3149 break 3150 3151 if instrument: 3152 self._ticker = instrument["ticker"] 3153 self._figi = instrument["figi"] 3154 3155 uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format( 3156 self._ticker, 3157 self._figi, 3158 int(instrument["volume"]), 3159 ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "", 3160 )) 3161 3162 tradeLots = abs(instrument["lots"]) - instrument["blocked"] # available volumes in lots for close operation 3163 3164 if tradeLots > 0: 3165 if instrument["blocked"] > 0: 3166 uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format( 3167 instrument["blocked"], 3168 self._ticker, 3169 tradeLots, 3170 )) 3171 3172 # if direction is "Long" then we need sell, if direction is "Short" then we need buy: 3173 self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots) 3174 3175 else: 3176 uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self._ticker))
Close position of given instruments.
Parameters
- instruments: list of instruments defined by tickers or FIGIs that must be closed.
- portfolio: pre-received dictionary with open trades, returned by
Overview()method. This avoids unnecessary downloading data from the server.
3178 def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None: 3179 """ 3180 Close all positions of given instruments with defined type. 3181 3182 :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list. 3183 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3184 This avoids unnecessary downloading data from the server. 3185 """ 3186 if iType not in TKS_INSTRUMENTS: 3187 uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType)) 3188 3189 else: 3190 if portfolio is None or not portfolio: 3191 portfolio = self.Overview(show=False) 3192 3193 tickers = [item["ticker"] for item in portfolio["stat"][iType]] 3194 uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers)) 3195 3196 if tickers and portfolio: 3197 self.CloseTrades(tickers, portfolio) 3198 3199 else: 3200 uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))
Close all positions of given instruments with defined type.
Parameters
- iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
- portfolio: pre-received dictionary with open trades, returned by
Overview()method. This avoids unnecessary downloading data from the server.
3202 def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3203 """ 3204 Universal method to create market or limit orders with all available parameters for current `accountId`. 3205 See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`. 3206 3207 If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above 3208 current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day. 3209 3210 Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" 3211 then broker immediately open market order as you can do simple --buy or --sell operations! 3212 3213 If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". 3214 When current price will go up or down to target price value then broker opens a limit order. 3215 Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter. 3216 3217 Only one attempt and no retry for opens order. If network issue occurred you can create new request. 3218 3219 :param operation: string "Buy" or "Sell". 3220 :param orderType: string "Limit" or "Stop". 3221 :param lots: volume, integer count of lots >= 1. 3222 :param targetPrice: target price > 0. This is open trade price for limit order. 3223 :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. 3224 Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order. 3225 :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types 3226 "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3227 Stop loss order always executed by market price. 3228 :param expDate: string "Undefined" by default or local date in future. 3229 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3230 This date is converting to UTC format for server. This parameter only makes sense for stop-order. 3231 A limit order has no expiration date, it lasts until the end of the trading day. 3232 :return: JSON with response from broker server. 3233 """ 3234 if self.accountId is None or not self.accountId: 3235 uLogger.error("Variable `accountId` must be defined for using this method!") 3236 raise Exception("Account ID required") 3237 3238 if operation is None or not operation or operation not in ("Buy", "Sell"): 3239 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 3240 raise Exception("Incorrect value") 3241 3242 if orderType is None or not orderType or orderType not in ("Limit", "Stop"): 3243 uLogger.error("You must define order type only one of them: `Limit` or `Stop`!") 3244 raise Exception("Incorrect value") 3245 3246 if lots is None or lots < 1: 3247 uLogger.error("You must define trade volume > 0: integer count of lots!") 3248 raise Exception("Incorrect value") 3249 3250 if targetPrice is None or targetPrice <= 0: 3251 uLogger.error("Target price for limit-order must be greater than 0!") 3252 raise Exception("Incorrect value") 3253 3254 if limitPrice is None or limitPrice <= 0: 3255 limitPrice = targetPrice 3256 3257 if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"): 3258 stopType = "Limit" 3259 3260 if expDate is None or not expDate: 3261 expDate = "Undefined" 3262 3263 if not (self._ticker or self._figi): 3264 uLogger.error("Tocker or FIGI must be defined!") 3265 raise Exception("Ticker or FIGI required") 3266 3267 response = {} 3268 instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True) 3269 self._ticker = instrument["ticker"] 3270 self._figi = instrument["figi"] 3271 3272 if orderType == "Limit": 3273 uLogger.debug( 3274 "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format( 3275 self._ticker, self._figi, 3276 operation, lots, targetPrice, instrument["currency"], 3277 )) 3278 3279 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3280 self.body = str({ 3281 "figi": self._figi, 3282 "quantity": str(lots), 3283 "price": FloatToNano(targetPrice), 3284 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3285 "accountId": str(self.accountId), 3286 "orderType": "ORDER_TYPE_LIMIT", # see: TKS_ORDER_TYPES 3287 }) 3288 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3289 3290 if "orderId" in response.keys(): 3291 uLogger.info( 3292 "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}]".format( 3293 response["orderId"], self._ticker, self._figi, operation, lots, 3294 "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"], 3295 )) 3296 3297 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3298 if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]: 3299 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format( 3300 targetPrice, instrument["currency"], 3301 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3302 )) 3303 3304 if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]: 3305 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format( 3306 targetPrice, instrument["currency"], 3307 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3308 )) 3309 3310 else: 3311 uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log and try again open order later.") 3312 3313 if orderType == "Stop": 3314 uLogger.debug( 3315 "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format( 3316 self._ticker, self._figi, 3317 operation, lots, 3318 targetPrice, instrument["currency"], 3319 limitPrice, instrument["currency"], 3320 stopType, expDate, 3321 )) 3322 3323 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder" 3324 expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT) 3325 stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT" 3326 3327 body = { 3328 "figi": self._figi, 3329 "quantity": str(lots), 3330 "price": FloatToNano(limitPrice), 3331 "stopPrice": FloatToNano(targetPrice), 3332 "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL", # see: TKS_STOP_ORDER_DIRECTIONS 3333 "accountId": str(self.accountId), 3334 "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL", # see: TKS_STOP_ORDER_EXPIRATION_TYPES 3335 "stopOrderType": stopOrderType, # see: TKS_STOP_ORDER_TYPES 3336 } 3337 3338 if expDateUTC: 3339 body["expireDate"] = expDateUTC 3340 3341 self.body = str(body) 3342 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3343 3344 if "stopOrderId" in response.keys(): 3345 uLogger.info( 3346 "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}], limit price [{} {}], stop-order type [{}] and expiration date [{} UTC]".format( 3347 response["stopOrderId"], self._ticker, self._figi, operation, lots, 3348 "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"], 3349 "{:.4f}".format(limitPrice).rstrip("0").rstrip("."), instrument["currency"], 3350 TKS_STOP_ORDER_TYPES[stopOrderType], 3351 datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"], 3352 )) 3353 3354 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3355 if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3356 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{} {}] is lower than the current price [{} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3357 "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"], 3358 "{:.4f}".format(instrument["currentPrice"]["lastPrice"]).rstrip("0").rstrip("."), instrument["currency"], 3359 )) 3360 3361 if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3362 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{} {}] is higher than the current price [{} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3363 "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"], 3364 "{:.4f}".format(instrument["currentPrice"]["lastPrice"]).rstrip("0").rstrip("."), instrument["currency"], 3365 )) 3366 3367 else: 3368 uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log and try again open order later.") 3369 3370 return response
Universal method to create market or limit orders with all available parameters for current accountId.
See more simple methods: BuyLimit(), BuyStop(), SellLimit(), SellStop().
If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" then broker immediately open market order as you can do simple --buy or --sell operations!
If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". When current price will go up or down to target price value then broker opens a limit order. Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
Only one attempt and no retry for opens order. If network issue occurred you can create new request.
Parameters
- operation: string "Buy" or "Sell".
- orderType: string "Limit" or "Stop".
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is open trade price for limit order.
- limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
- stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. Stop loss order always executed by market price.
- expDate: string "Undefined" by default or local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S. This date is converting to UTC format for server. This parameter only makes sense for stop-order. A limit order has no expiration date, it lasts until the end of the trading day.
Returns
JSON with response from broker server.
3372 def BuyLimit(self, lots: int, targetPrice: float) -> dict: 3373 """ 3374 Create pending `Buy` limit-order (below current price). You must specify only 2 parameters: 3375 `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then 3376 broker immediately open `Buy` market order, such as if you do simple `--buy` operation! 3377 See also: `Order()` docstring. 3378 3379 :param lots: volume, integer count of lots >= 1. 3380 :param targetPrice: target price > 0. This is open trade price for limit order. 3381 :return: JSON with response from broker server. 3382 """ 3383 return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)
Create pending Buy limit-order (below current price). You must specify only 2 parameters:
lots and target price to open buy limit-order. If you try to create buy limit-order above current price then
broker immediately open Buy market order, such as if you do simple --buy operation!
See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is open trade price for limit order.
Returns
JSON with response from broker server.
3385 def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3386 """ 3387 Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order. 3388 In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3389 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3390 target price value then broker opens a limit order. See also: `Order()` docstring. 3391 3392 :param lots: volume, integer count of lots >= 1. 3393 :param targetPrice: target price > 0. This is trigger price for buy stop-order. 3394 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3395 with price equal to limitPrice, when current price goes to target price of buy stop-order. 3396 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3397 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3398 :param expDate: string "Undefined" by default or local date in future. 3399 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3400 This date is converting to UTC format for server. 3401 :return: JSON with response from broker server. 3402 """ 3403 return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
Create Buy stop-order. You must specify at least 2 parameters: lots target price to open buy stop-order.
In additional you can specify 3 parameters for buy stop-order: limit price >=0, stop type = Limit|SL|TP,
expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to
target price value then broker opens a limit order. See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is trigger price for buy stop-order.
- limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of buy stop-order.
- stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
- expDate: string "Undefined" by default or local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns
JSON with response from broker server.
3405 def SellLimit(self, lots: int, targetPrice: float) -> dict: 3406 """ 3407 Create pending `Sell` limit-order (above current price). You must specify only 2 parameters: 3408 `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then 3409 broker immediately open `Sell` market order, such as if you do simple `--sell` operation! 3410 See also: `Order()` docstring. 3411 3412 :param lots: volume, integer count of lots >= 1. 3413 :param targetPrice: target price > 0. This is open trade price for limit order. 3414 :return: JSON with response from broker server. 3415 """ 3416 return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)
Create pending Sell limit-order (above current price). You must specify only 2 parameters:
lots and target price to open sell limit-order. If you try to create sell limit-order below current price then
broker immediately open Sell market order, such as if you do simple --sell operation!
See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is open trade price for limit order.
Returns
JSON with response from broker server.
3418 def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3419 """ 3420 Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order. 3421 In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3422 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3423 target price value then broker opens a limit order. See also: `Order()` docstring. 3424 3425 :param lots: volume, integer count of lots >= 1. 3426 :param targetPrice: target price > 0. This is trigger price for sell stop-order. 3427 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3428 with price equal to limitPrice, when current price goes to target price of sell stop-order. 3429 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3430 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3431 :param expDate: string "Undefined" by default or local date in future. 3432 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3433 This date is converting to UTC format for server. 3434 :return: JSON with response from broker server. 3435 """ 3436 return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
Create Sell stop-order. You must specify at least 2 parameters: lots target price to open sell stop-order.
In additional you can specify 3 parameters for sell stop-order: limit price >=0, stop type = Limit|SL|TP,
expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to
target price value then broker opens a limit order. See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is trigger price for sell stop-order.
- limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of sell stop-order.
- stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
- expDate: string "Undefined" by default or local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns
JSON with response from broker server.
3438 def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None: 3439 """ 3440 Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`. 3441 3442 :param orderIDs: list of integers with `orderId` or `stopOrderId`. 3443 :param allOrdersIDs: pre-received lists of all active pending limit orders. 3444 This avoids unnecessary downloading data from the server. 3445 :param allStopOrdersIDs: pre-received lists of all active stop orders. 3446 """ 3447 if self.accountId is None or not self.accountId: 3448 uLogger.error("Variable `accountId` must be defined for using this method!") 3449 raise Exception("Account ID required") 3450 3451 if orderIDs: 3452 if allOrdersIDs is None: 3453 rawOrders = self.RequestPendingOrders() 3454 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending limit orders ID 3455 3456 if allStopOrdersIDs is None: 3457 rawStopOrders = self.RequestStopOrders() 3458 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3459 3460 for orderID in orderIDs: 3461 idInPendingOrders = orderID in allOrdersIDs 3462 idInStopOrders = orderID in allStopOrdersIDs 3463 3464 if not (idInPendingOrders or idInStopOrders): 3465 uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID)) 3466 continue 3467 3468 else: 3469 if idInPendingOrders: 3470 uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID)) 3471 3472 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder 3473 self.body = str({"accountId": self.accountId, "orderId": orderID}) 3474 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder" 3475 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3476 3477 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3478 if self.moreDebug: 3479 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3480 3481 uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID)) 3482 3483 else: 3484 uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID)) 3485 3486 elif idInStopOrders: 3487 uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID)) 3488 3489 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder 3490 self.body = str({"accountId": self.accountId, "stopOrderId": orderID}) 3491 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder" 3492 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3493 3494 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3495 if self.moreDebug: 3496 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3497 3498 uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID)) 3499 3500 else: 3501 uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID)) 3502 3503 else: 3504 continue
Cancel order or list of orders by its orderId or stopOrderId for current accountId.
Parameters
- orderIDs: list of integers with
orderIdorstopOrderId. - allOrdersIDs: pre-received lists of all active pending limit orders. This avoids unnecessary downloading data from the server.
- allStopOrdersIDs: pre-received lists of all active stop orders.
3506 def CloseAllOrders(self) -> None: 3507 """ 3508 Gets a list of open pending and stop orders and cancel it all. 3509 """ 3510 rawOrders = self.RequestPendingOrders() 3511 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending limit orders ID 3512 lenOrders = len(allOrdersIDs) 3513 3514 rawStopOrders = self.RequestStopOrders() 3515 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3516 lenSOrders = len(allStopOrdersIDs) 3517 3518 if lenOrders > 0 or lenSOrders > 0: 3519 uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders)) 3520 3521 self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs) 3522 3523 else: 3524 uLogger.info("Orders not found, nothing to cancel.")
Gets a list of open pending and stop orders and cancel it all.
3526 def CloseAll(self, *args) -> None: 3527 """ 3528 Close all available (not blocked) opened trades and orders. 3529 3530 Also, you can select one or more keywords case-insensitive: 3531 `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type. 3532 3533 Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods. 3534 """ 3535 overview = self.Overview(show=False) # get all open trades info 3536 3537 if len(args) == 0: 3538 uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...") 3539 self.CloseAllOrders() # close all pending and stop orders 3540 3541 for iType in TKS_INSTRUMENTS: 3542 if iType != "Currencies": 3543 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3544 3545 else: 3546 uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args))) 3547 lowerArgs = [x.lower() for x in args] 3548 3549 if "orders" in lowerArgs: 3550 self.CloseAllOrders() # close all pending and stop orders 3551 3552 for iType in TKS_INSTRUMENTS: 3553 if iType.lower() in lowerArgs and iType != "Currencies": 3554 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies
Close all available (not blocked) opened trades and orders.
Also, you can select one or more keywords case-insensitive:
orders, shares, bonds, etfs and futures from TKS_INSTRUMENTS enum to specify trades type.
Currency positions you must close manually using buy or sell operations, CloseTrades() or CloseAllTrades() methods.
3556 def CloseAllByTicker(self, instrument: str) -> None: 3557 """ 3558 Close all available (not blocked) opened trades and orders for one instrument defined by its ticker. 3559 3560 This method searches opened trade and orders of instrument throw all portfolio and then use 3561 `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument. 3562 3563 See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`. 3564 3565 :param instrument: string with ticker. 3566 """ 3567 if instrument is None or not instrument: 3568 uLogger.error("Ticker name must be defined for using this method!") 3569 raise Exception("Ticker required") 3570 3571 overview = self.Overview(show=False) # get user portfolio with all open trades info 3572 3573 self._ticker = instrument # try to set instrument as ticker 3574 self._figi = "" 3575 3576 limitAll = [item["orderID"] for item in overview["stat"]["orders"]] # list of all pending limit order IDs 3577 stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]] # list of all stop order IDs 3578 3579 if limitAll and self.IsInLimitOrders(portfolio=overview): 3580 uLogger.debug("Closing all opened pending limit orders for the instrument with ticker [{}]. Wait, please...") 3581 self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3582 3583 if stopAll and self.IsInStopOrders(portfolio=overview): 3584 uLogger.debug("Closing all opened stop orders for the instrument with ticker [{}]. Wait, please...") 3585 self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3586 3587 if self.IsInPortfolio(portfolio=overview): 3588 uLogger.debug("Closing all available (not blocked) opened trade for the instrument with ticker [{}]. Wait, please...") 3589 self.CloseTrades(instruments=[instrument], portfolio=overview)
Close all available (not blocked) opened trades and orders for one instrument defined by its ticker.
This method searches opened trade and orders of instrument throw all portfolio and then use
CloseTrades() and CloseOrders() methods to close trade and cancel all orders for that instrument.
See also: IsInLimitOrders(), GetLimitOrderIDs(), IsInStopOrders(), GetStopOrderIDs(), CloseTrades() and CloseOrders().
Parameters
- instrument: string with ticker.
3591 def CloseAllByFIGI(self, instrument: str) -> None: 3592 """ 3593 Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id. 3594 3595 This method searches opened trade and orders of instrument throw all portfolio and then use 3596 `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument. 3597 3598 See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`. 3599 3600 :param instrument: string with FIGI id. 3601 """ 3602 if instrument is None or not instrument: 3603 uLogger.error("FIGI id must be defined for using this method!") 3604 raise Exception("FIGI required") 3605 3606 overview = self.Overview(show=False) # get user portfolio with all open trades info 3607 3608 self._ticker = "" 3609 self._figi = instrument # try to set instrument as FIGI id 3610 3611 limitAll = [item["orderID"] for item in overview["stat"]["orders"]] # list of all pending limit order IDs 3612 stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]] # list of all stop order IDs 3613 3614 if limitAll and self.IsInLimitOrders(portfolio=overview): 3615 uLogger.debug("Closing all opened pending limit orders for the instrument with FIGI [{}]. Wait, please...") 3616 self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3617 3618 if stopAll and self.IsInStopOrders(portfolio=overview): 3619 uLogger.debug("Closing all opened stop orders for the instrument with FIGI [{}]. Wait, please...") 3620 self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3621 3622 if self.IsInPortfolio(portfolio=overview): 3623 uLogger.debug("Closing all available (not blocked) opened trade for the instrument with FIGI [{}]. Wait, please...") 3624 self.CloseTrades(instruments=[instrument], portfolio=overview)
Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id.
This method searches opened trade and orders of instrument throw all portfolio and then use
CloseTrades() and CloseOrders() methods to close trade and cancel all orders for that instrument.
See also: IsInLimitOrders(), GetLimitOrderIDs(), IsInStopOrders(), GetStopOrderIDs(), CloseTrades() and CloseOrders().
Parameters
- instrument: string with FIGI id.
3626 @staticmethod 3627 def ParseOrderParameters(operation, **inputParameters): 3628 """ 3629 Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders. 3630 3631 :param operation: string "Buy" or "Sell". 3632 :param inputParameters: this is dict of strings that looks like this 3633 `{"lots": "L_int,...", "prices": "P_float,..."}` where 3634 "lots" key: one or more lot values (integer numbers) to open with every limit-order 3635 "prices" key: one or more prices to open limit-orders 3636 Counts of values in lots and prices lists must be equals! 3637 :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]` 3638 """ 3639 # TODO: update order grid work with api v2 3640 pass 3641 # uLogger.debug("Input parameters: {}".format(inputParameters)) 3642 # 3643 # if operation is None or not operation or operation not in ("Buy", "Sell"): 3644 # uLogger.error("You must define operation type: 'Buy' or 'Sell'!") 3645 # raise Exception("Incorrect value") 3646 # 3647 # if "l" in inputParameters.keys(): 3648 # inputParameters["lots"] = inputParameters.pop("l") 3649 # 3650 # if "p" in inputParameters.keys(): 3651 # inputParameters["prices"] = inputParameters.pop("p") 3652 # 3653 # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys(): 3654 # uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!") 3655 # raise Exception("Incorrect value") 3656 # 3657 # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")] 3658 # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")] 3659 # 3660 # if len(lots) != len(prices): 3661 # uLogger.error("'lots' and 'prices' lists must have equal length of values!") 3662 # raise Exception("Incorrect value") 3663 # 3664 # uLogger.debug("Extracted parameters for orders:") 3665 # uLogger.debug("lots = {}".format(lots)) 3666 # uLogger.debug("prices = {}".format(prices)) 3667 # 3668 # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...] 3669 # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))] 3670 # uLogger.debug("Order parameters: {}".format(result)) 3671 # 3672 # return result
Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
Parameters
- operation: string "Buy" or "Sell".
- inputParameters: this is dict of strings that looks like this
{"lots": "L_int,...", "prices": "P_float,..."}where "lots" key: one or more lot values (integer numbers) to open with every limit-order "prices" key: one or more prices to open limit-orders Counts of values in lots and prices lists must be equals!
Returns
list of dictionaries with all lots and prices to open orders that looks like this
[{"lot": lots_1, "price": price_1}, {...}, ...]
3674 def IsInPortfolio(self, portfolio: dict = None) -> bool: 3675 """ 3676 Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`. 3677 3678 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3679 :return: `True` if portfolio contains open position with given instrument, `False` otherwise. 3680 """ 3681 result = False 3682 msg = "Instrument not defined!" 3683 3684 if portfolio is None or not portfolio: 3685 portfolio = self.Overview(show=False) 3686 3687 if self._ticker: 3688 uLogger.debug("Searching instrument with ticker [{}] throw opened positions list...".format(self._ticker)) 3689 msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker) 3690 3691 for iType in TKS_INSTRUMENTS: 3692 for instrument in portfolio["stat"][iType]: 3693 if instrument["ticker"] == self._ticker: 3694 result = True 3695 msg = "Instrument with ticker [{}] is present in open positions".format(self._ticker) 3696 break 3697 3698 elif self._figi: 3699 uLogger.debug("Searching instrument with FIGI [{}] throw opened positions list...".format(self._figi)) 3700 msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi) 3701 3702 for iType in TKS_INSTRUMENTS: 3703 for instrument in portfolio["stat"][iType]: 3704 if instrument["figi"] == self._figi: 3705 result = True 3706 msg = "Instrument with FIGI [{}] is present in open positions".format(self._figi) 3707 break 3708 3709 else: 3710 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3711 3712 uLogger.debug(msg) 3713 3714 return result
Checks if instrument is in the user's portfolio. Instrument must be defined by ticker (highly priority) or figi.
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
Trueif portfolio contains open position with given instrument,Falseotherwise.
3716 def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict: 3717 """ 3718 Returns instrument from the user's portfolio if it presents there. 3719 Instrument must be defined by `ticker` (highly priority) or `figi`. 3720 3721 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3722 :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise. 3723 """ 3724 result = None 3725 msg = "Instrument not defined!" 3726 3727 if portfolio is None or not portfolio: 3728 portfolio = self.Overview(show=False) 3729 3730 if self._ticker: 3731 uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self._ticker)) 3732 msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker) 3733 3734 for iType in TKS_INSTRUMENTS: 3735 for instrument in portfolio["stat"][iType]: 3736 if instrument["ticker"] == self._ticker: 3737 result = instrument 3738 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self._ticker, instrument["figi"]) 3739 break 3740 3741 elif self._figi: 3742 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self._figi)) 3743 msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi) 3744 3745 for iType in TKS_INSTRUMENTS: 3746 for instrument in portfolio["stat"][iType]: 3747 if instrument["figi"] == self._figi: 3748 result = instrument 3749 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self._figi) 3750 break 3751 3752 else: 3753 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3754 3755 uLogger.debug(msg) 3756 3757 return result
Returns instrument from the user's portfolio if it presents there.
Instrument must be defined by ticker (highly priority) or figi.
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
dict with instrument if portfolio contains open position with this instrument,
Noneotherwise.
3759 def IsInLimitOrders(self, portfolio: dict = None) -> bool: 3760 """ 3761 Checks if instrument is in the limit orders list. Instrument must be defined by `ticker` (highly priority) or `figi`. 3762 3763 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3764 3765 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3766 :return: `True` if limit orders list contains some limit orders for the instrument, `False` otherwise. 3767 """ 3768 result = False 3769 msg = "Instrument not defined!" 3770 3771 if portfolio is None or not portfolio: 3772 portfolio = self.Overview(show=False) 3773 3774 if self._ticker: 3775 uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker)) 3776 msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker) 3777 3778 for instrument in portfolio["stat"]["orders"]: 3779 if instrument["ticker"] == self._ticker: 3780 result = True 3781 msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker) 3782 break 3783 3784 elif self._figi: 3785 uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi)) 3786 msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi) 3787 3788 for instrument in portfolio["stat"]["orders"]: 3789 if instrument["figi"] == self._figi: 3790 result = True 3791 msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi) 3792 break 3793 3794 else: 3795 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3796 3797 uLogger.debug(msg) 3798 3799 return result
Checks if instrument is in the limit orders list. Instrument must be defined by ticker (highly priority) or figi.
See also: CloseAllByTicker() and CloseAllByFIGI().
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
Trueif limit orders list contains some limit orders for the instrument,Falseotherwise.
3801 def GetLimitOrderIDs(self, portfolio: dict = None) -> list[str]: 3802 """ 3803 Returns list with all `orderID`s of opened pending limit orders for the instrument. 3804 Instrument must be defined by `ticker` (highly priority) or `figi`. 3805 3806 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3807 3808 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3809 :return: list with `orderID`s of limit orders. 3810 """ 3811 result = [] 3812 msg = "Instrument not defined!" 3813 3814 if portfolio is None or not portfolio: 3815 portfolio = self.Overview(show=False) 3816 3817 if self._ticker: 3818 uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker)) 3819 msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker) 3820 3821 for instrument in portfolio["stat"]["orders"]: 3822 if instrument["ticker"] == self._ticker: 3823 result.append(instrument["orderID"]) 3824 3825 if result: 3826 msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker) 3827 3828 elif self._figi: 3829 uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi)) 3830 msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi) 3831 3832 for instrument in portfolio["stat"]["orders"]: 3833 if instrument["figi"] == self._figi: 3834 result.append(instrument["orderID"]) 3835 3836 if result: 3837 msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi) 3838 3839 else: 3840 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3841 3842 uLogger.debug(msg) 3843 3844 return result
Returns list with all orderIDs of opened pending limit orders for the instrument.
Instrument must be defined by ticker (highly priority) or figi.
See also: CloseAllByTicker() and CloseAllByFIGI().
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
list with
orderIDs of limit orders.
3846 def IsInStopOrders(self, portfolio: dict = None) -> bool: 3847 """ 3848 Checks if instrument is in the stop orders list. Instrument must be defined by `ticker` (highly priority) or `figi`. 3849 3850 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3851 3852 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3853 :return: `True` if stop orders list contains some stop orders for the instrument, `False` otherwise. 3854 """ 3855 result = False 3856 msg = "Instrument not defined!" 3857 3858 if portfolio is None or not portfolio: 3859 portfolio = self.Overview(show=False) 3860 3861 if self._ticker: 3862 uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker)) 3863 msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker) 3864 3865 for instrument in portfolio["stat"]["stopOrders"]: 3866 if instrument["ticker"] == self._ticker: 3867 result = True 3868 msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker) 3869 break 3870 3871 elif self._figi: 3872 uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi)) 3873 msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi) 3874 3875 for instrument in portfolio["stat"]["stopOrders"]: 3876 if instrument["figi"] == self._figi: 3877 result = True 3878 msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi) 3879 break 3880 3881 else: 3882 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3883 3884 uLogger.debug(msg) 3885 3886 return result
Checks if instrument is in the stop orders list. Instrument must be defined by ticker (highly priority) or figi.
See also: CloseAllByTicker() and CloseAllByFIGI().
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
Trueif stop orders list contains some stop orders for the instrument,Falseotherwise.
3888 def GetStopOrderIDs(self, portfolio: dict = None) -> list[str]: 3889 """ 3890 Returns list with all `orderID`s of opened stop orders for the instrument. 3891 Instrument must be defined by `ticker` (highly priority) or `figi`. 3892 3893 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3894 3895 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3896 :return: list with `orderID`s of stop orders. 3897 """ 3898 result = [] 3899 msg = "Instrument not defined!" 3900 3901 if portfolio is None or not portfolio: 3902 portfolio = self.Overview(show=False) 3903 3904 if self._ticker: 3905 uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker)) 3906 msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker) 3907 3908 for instrument in portfolio["stat"]["stopOrders"]: 3909 if instrument["ticker"] == self._ticker: 3910 result.append(instrument["orderID"]) 3911 3912 if result: 3913 msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker) 3914 3915 elif self._figi: 3916 uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi)) 3917 msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi) 3918 3919 for instrument in portfolio["stat"]["stopOrders"]: 3920 if instrument["figi"] == self._figi: 3921 result.append(instrument["orderID"]) 3922 3923 if result: 3924 msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi) 3925 3926 else: 3927 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3928 3929 uLogger.debug(msg) 3930 3931 return result
Returns list with all orderIDs of opened stop orders for the instrument.
Instrument must be defined by ticker (highly priority) or figi.
See also: CloseAllByTicker() and CloseAllByFIGI().
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
list with
orderIDs of stop orders.
3933 def RequestLimits(self) -> dict: 3934 """ 3935 Method for obtaining the available funds for withdrawal for current `accountId`. 3936 3937 See also: 3938 - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits 3939 - `OverviewLimits()` method 3940 3941 :return: dict with raw data from server that contains free funds for withdrawal. Example of dict: 3942 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`. 3943 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency 3944 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures. 3945 """ 3946 if self.accountId is None or not self.accountId: 3947 uLogger.error("Variable `accountId` must be defined for using this method!") 3948 raise Exception("Account ID required") 3949 3950 uLogger.debug("Requesting current available funds for withdrawal. Wait, please...") 3951 3952 self.body = str({"accountId": self.accountId}) 3953 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits" 3954 rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3955 3956 if self.moreDebug: 3957 uLogger.debug("Records about available funds for withdrawal successfully received") 3958 3959 return rawLimits
Method for obtaining the available funds for withdrawal for current accountId.
See also:
- REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
OverviewLimits()method
Returns
dict with raw data from server that contains free funds for withdrawal. Example of dict:
{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}. Heremoneyis an array of portfolio currency positions,blockedis an array of blocked currency positions of the portfolio andblockedGuaranteeis locked money under collateral for futures.
3961 def OverviewLimits(self, show: bool = False, onlyFiles=False) -> dict: 3962 """ 3963 Method for parsing and show table with available funds for withdrawal for current `accountId`. 3964 3965 See also: `RequestLimits()`. 3966 3967 :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log. 3968 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 3969 :return: dict with raw parsed data from server and some calculated statistics about it. 3970 """ 3971 if self.accountId is None or not self.accountId: 3972 uLogger.error("Variable `accountId` must be defined for using this method!") 3973 raise Exception("Account ID required") 3974 3975 rawLimits = self.RequestLimits() # raw response with current available funds for withdrawal 3976 3977 view = { 3978 "rawLimits": rawLimits, 3979 "limits": { # parsed data for every currency: 3980 "money": { # this is an array of portfolio currency positions 3981 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"] 3982 }, 3983 "blocked": { # this is an array of blocked currency 3984 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"] 3985 }, 3986 "blockedGuarantee": { # this is locked money under collateral for futures 3987 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"] 3988 }, 3989 }, 3990 } 3991 3992 # --- Prepare text table with limits in human-readable format: 3993 if show or onlyFiles: 3994 info = [ 3995 "# Withdrawal limits\n\n", 3996 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 3997 "* **Account ID:** [{}]\n".format(self.accountId), 3998 ] 3999 4000 if view["limits"]["money"]: 4001 info.extend([ 4002 "\n| Currencies | Total | Available for withdrawal | Blocked for trade | Futures guarantee |\n", 4003 "|------------|---------------|--------------------------|-------------------|-------------------|\n", 4004 ]) 4005 4006 else: 4007 info.append("\nNo withdrawal limits\n") 4008 4009 for curr in view["limits"]["money"].keys(): 4010 blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0 4011 blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0 4012 availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee) 4013 4014 infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format( 4015 "[{}]".format(curr), 4016 "{:.2f}".format(view["limits"]["money"][curr]), 4017 "{:.2f}".format(availableMoney), 4018 "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—", 4019 "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—", 4020 ) 4021 4022 if curr == "rub": 4023 info.insert(5, infoStr) # hack: insert "rub" at the first position in table and after headers 4024 4025 else: 4026 info.append(infoStr) 4027 4028 infoText = "".join(info) 4029 4030 if show and not onlyFiles: 4031 uLogger.info(infoText) 4032 4033 if self.withdrawalLimitsFile and (show or onlyFiles): 4034 with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH: 4035 fH.write(infoText) 4036 4037 uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile))) 4038 4039 if self.useHTMLReports: 4040 htmlFilePath = self.withdrawalLimitsFile.replace(".md", ".html") if self.withdrawalLimitsFile.endswith(".md") else self.withdrawalLimitsFile + ".html" 4041 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4042 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Withdrawal limits", commonCSS=COMMON_CSS, markdown=infoText)) 4043 4044 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4045 4046 return view
Method for parsing and show table with available funds for withdrawal for current accountId.
See also: RequestLimits().
Parameters
- show: if
Falsethen only dictionary returns, ifTruethen also print withdrawal limits to log. - onlyFiles: if
Truethen do not show Markdown table in the console, but only generates report files.
Returns
dict with raw parsed data from server and some calculated statistics about it.
4048 def RequestAccounts(self) -> dict: 4049 """ 4050 Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`. 4051 4052 See also: 4053 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts 4054 - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account 4055 - `OverviewUserInfo()` method 4056 4057 :return: dict with raw data from server that contains accounts info. Example of dict: 4058 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", 4059 "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", 4060 "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`. 4061 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now. 4062 """ 4063 uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...") 4064 4065 self.body = str({}) 4066 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts" 4067 rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST") 4068 4069 if self.moreDebug: 4070 uLogger.debug("Records about available accounts successfully received") 4071 4072 return rawAccounts
Method for requesting all brokerage accounts (accountIds) of current user detected by token.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
- What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
OverviewUserInfo()method
Returns
dict with raw data from server that contains accounts info. Example of dict:
{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}. IfclosedDate="1970-01-01T00:00:00Z"it means that account is active now.
4074 def RequestUserInfo(self) -> dict: 4075 """ 4076 Method for requesting common user's information. 4077 4078 See also: 4079 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo 4080 - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest 4081 - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with 4082 - `OverviewUserInfo()` method 4083 4084 :return: dict with raw data from server that contains user's information. Example of dict: 4085 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", 4086 "russian_shares", "structured_income_bonds"], "tariff": "premium"}`. 4087 """ 4088 uLogger.debug("Requesting common user's information. Wait, please...") 4089 4090 self.body = str({}) 4091 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo" 4092 rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST") 4093 4094 if self.moreDebug: 4095 uLogger.debug("Records about current user successfully received") 4096 4097 return rawUserInfo
Method for requesting common user's information.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
- What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
- What does
qualified_for_work_withfield mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with OverviewUserInfo()method
Returns
dict with raw data from server that contains user's information. Example of dict:
{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", "russian_shares", "structured_income_bonds"], "tariff": "premium"}.
4099 def RequestMarginStatus(self, accountId: str = None) -> dict: 4100 """ 4101 Method for requesting margin calculation for defined account ID. 4102 4103 See also: 4104 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes 4105 - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse 4106 - `OverviewUserInfo()` method 4107 4108 :param accountId: string with numeric account ID. If `None`, then used class field `accountId`. 4109 :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. 4110 Example of responses: 4111 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`. 4112 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, 4113 "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, 4114 "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, 4115 "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, 4116 "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`. 4117 """ 4118 if accountId is None or not accountId: 4119 if self.accountId is None or not self.accountId: 4120 uLogger.error("Variable `accountId` must be defined for using this method!") 4121 raise Exception("Account ID required") 4122 4123 else: 4124 accountId = self.accountId # use `self.accountId` (main ID) by default 4125 4126 uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId)) 4127 4128 self.body = str({"accountId": accountId}) 4129 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes" 4130 rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST") 4131 4132 if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}: 4133 uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId)) 4134 rawMargin = {} 4135 4136 else: 4137 if self.moreDebug: 4138 uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId)) 4139 4140 return rawMargin
Method for requesting margin calculation for defined account ID.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
- What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
OverviewUserInfo()method
Parameters
- accountId: string with numeric account ID. If
None, then used class fieldaccountId.
Returns
dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. Example of responses: status code 400:
{"code": 3, "message": "account margin status is disabled", "description": "30051" }, returns:{}. status code 200:{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}.
4142 def RequestTariffLimits(self) -> dict: 4143 """ 4144 Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`. 4145 4146 See also: 4147 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff 4148 - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest 4149 - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit 4150 - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit 4151 - `OverviewUserInfo()` method 4152 4153 :return: dict with raw data from server that contains limits of current tariff. Example of dict: 4154 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], 4155 "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`. 4156 """ 4157 uLogger.debug("Requesting limits of current tariff. Wait, please...") 4158 4159 self.body = str({}) 4160 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff" 4161 rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 4162 4163 if self.moreDebug: 4164 uLogger.debug("Records with limits of current tariff successfully received") 4165 4166 return rawTariffLimits
Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by token.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
- What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
- Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
- Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
OverviewUserInfo()method
Returns
dict with raw data from server that contains limits of current tariff. Example of dict:
{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}.
4168 def RequestBondCoupons(self, iJSON: dict) -> dict: 4169 """ 4170 Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown 4171 then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`. 4172 All dates are in UTC timezone. 4173 4174 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons 4175 Documentation: 4176 - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest 4177 - response: https://tinkoff.github.io/investAPI/instruments/#coupon 4178 4179 See also: `ExtendBondsData()`. 4180 4181 :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self._ticker]` 4182 If raw iJSON is not data of bond then server returns an error [400] with message: 4183 `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`. 4184 :return: dictionary with bond payment calendar. Response example 4185 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", 4186 "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, 4187 "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", 4188 "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}` 4189 """ 4190 if iJSON["figi"] is None or not iJSON["figi"]: 4191 uLogger.error("FIGI must be defined for using this method!") 4192 raise Exception("FIGI required") 4193 4194 startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z" 4195 endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z" 4196 4197 uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format( 4198 "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "", 4199 self._figi, 4200 startDate, 4201 endDate, 4202 )) 4203 4204 self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate}) 4205 calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons" 4206 calendar = self.SendAPIRequest(calendarURL, reqType="POST") 4207 4208 if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}: 4209 uLogger.warning("Instrument type is not bond!") 4210 4211 else: 4212 if self.moreDebug: 4213 uLogger.debug("Records about bond payment calendar successfully received") 4214 4215 return calendar
Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
then requesting dates "from": "1970-01-01T00:00:00.000Z" and "to": "2099-12-31T23:59:59.000Z".
All dates are in UTC timezone.
REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons Documentation:
- request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
- response: https://tinkoff.github.io/investAPI/instruments/#coupon
See also: ExtendBondsData().
Parameters
- iJSON: raw json data of a bond from broker server, example
iJSON = self.iList["Bonds"][self._ticker]If raw iJSON is not data of bond then server returns an error [400] with message:{"code": 3, "message": "instrument type is not bond", "description": "30048"}.
Returns
dictionary with bond payment calendar. Response example
{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}
4217 def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame: 4218 """ 4219 Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider 4220 Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, 4221 coupon yields, current yields and some statistics etc. 4222 4223 WARNING! This is too long operation if a lot of bonds requested from broker server. 4224 4225 See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`. 4226 4227 :param instruments: list of strings with tickers or FIGIs. 4228 :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`, 4229 for further used by data scientists or stock analytics. 4230 :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. 4231 In XLSX-file and Pandas DataFrame fields mean: 4232 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond 4233 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon 4234 """ 4235 if instruments is None or not instruments: 4236 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 4237 raise Exception("Ticker or FIGI required") 4238 4239 if isinstance(instruments, str): 4240 instruments = [instruments] 4241 4242 uniqueInstruments = self.GetUniqueFIGIs(instruments) 4243 4244 uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...") 4245 4246 iCount = len(uniqueInstruments) 4247 tooLong = iCount >= 20 4248 if tooLong: 4249 uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...") 4250 4251 bonds = None 4252 for i, self._figi in enumerate(uniqueInstruments): 4253 instrument = self.SearchByFIGI(requestPrice=False) # raw data about instrument from server 4254 4255 if "type" in instrument.keys() and instrument["type"] == "Bonds": 4256 # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond 4257 rawBond = self.SearchByFIGI(requestPrice=True) 4258 4259 # Widen raw data with UTC current time (iData["actualDateTime"]): 4260 actualDate = datetime.now(tzutc()) 4261 iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond 4262 4263 # Widen raw data with bond payment calendar (iData["rawCalendar"]): 4264 iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)} 4265 4266 # Replace some values with human-readable: 4267 iData["nominalCurrency"] = iData["nominal"]["currency"] 4268 iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"]) 4269 iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"]) 4270 iData["aciCurrency"] = iData["aciValue"]["currency"] 4271 iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"]) 4272 iData["issueSize"] = int(iData["issueSize"]) 4273 iData["issueSizePlan"] = int(iData["issueSizePlan"]) 4274 iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]] 4275 iData["step"] = iData["step"] if "step" in iData.keys() else 0 4276 iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]] 4277 iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0 4278 iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0 4279 iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0 4280 iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0 4281 iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0 4282 iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0 4283 4284 # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date): 4285 iData["limitUpPercent"] = iData["currentPrice"]["limitUp"] # max price on current day in percents of nominal 4286 iData["limitDownPercent"] = iData["currentPrice"]["limitDown"] # min price on current day in percents of nominal 4287 iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"] # last price on market in percents of nominal 4288 iData["closePricePercent"] = iData["currentPrice"]["closePrice"] # previous day close in percents of nominal 4289 iData["changes"] = iData["currentPrice"]["changes"] # this is percent of changes between `currentPrice` and `lastPrice` 4290 iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100 # max price on current day is `limitUpPercent` * `nominal` 4291 iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100 # min price on current day is `limitDownPercent` * `nominal` 4292 iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100 # last price on market is `lastPricePercent` * `nominal` 4293 iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100 # previous day close is `closePricePercent` * `nominal` 4294 iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"] # this is delta between last deal price and last close 4295 4296 # Widen raw data with calendar data from `rawCalendar` values: 4297 calendarData = [] 4298 if "events" in iData["rawCalendar"].keys(): 4299 for item in iData["rawCalendar"]["events"]: 4300 calendarData.append({ 4301 "couponDate": item["couponDate"], 4302 "couponNumber": int(item["couponNumber"]), 4303 "fixDate": item["fixDate"] if "fixDate" in item.keys() else "", 4304 "payCurrency": item["payOneBond"]["currency"], 4305 "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]), 4306 "couponType": TKS_COUPON_TYPES[item["couponType"]], 4307 "couponStartDate": item["couponStartDate"], 4308 "couponEndDate": item["couponEndDate"], 4309 "couponPeriod": item["couponPeriod"], 4310 }) 4311 4312 # if maturity date is unknown then uses the latest date in bond payment calendar for it: 4313 if "maturityDate" not in iData.keys(): 4314 iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else "" 4315 4316 # Widen raw data with Coupon Rate. 4317 # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%: 4318 iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData]) 4319 iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData]) 4320 iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0. 4321 4322 # Widen raw data with Yield to Maturity (YTM) on current date. 4323 # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%: 4324 maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None 4325 iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None 4326 iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate]) 4327 iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"] # sum of all last coupons minus current ACI value 4328 iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0. 4329 4330 iData["calendar"] = calendarData # adds calendar at the end 4331 4332 # Remove not used data: 4333 iData.pop("uid") 4334 iData.pop("positionUid") 4335 iData.pop("currentPrice") 4336 iData.pop("rawCalendar") 4337 4338 colNames = list(iData.keys()) 4339 if bonds is None: 4340 bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames)) 4341 4342 else: 4343 bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True) 4344 4345 else: 4346 uLogger.warning("Instrument is not a bond!") 4347 4348 processed = round(100 * (i + 1) / iCount, 1) 4349 if tooLong and processed % 5 == 0: 4350 uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount)) 4351 4352 else: 4353 uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount)) 4354 4355 bonds.index = bonds["ticker"].tolist() # replace indexes with ticker names 4356 4357 # Saving bonds from Pandas DataFrame to XLSX sheet: 4358 if xlsx and self.bondsXLSXFile: 4359 with pd.ExcelWriter( 4360 path=self.bondsXLSXFile, 4361 date_format=TKS_DATE_FORMAT, 4362 datetime_format=TKS_DATE_TIME_FORMAT, 4363 mode="w", 4364 ) as writer: 4365 bonds.to_excel( 4366 writer, 4367 sheet_name="Extended bonds data", 4368 index=True, 4369 encoding="UTF-8", 4370 freeze_panes=(1, 1), 4371 ) # saving as XLSX-file with freeze first row and column as headers 4372 4373 uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile))) 4374 4375 return bonds
Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc.
WARNING! This is too long operation if a lot of bonds requested from broker server.
See also: ShowInstrumentInfo(), CreateBondsCalendar(), ShowBondsCalendar(), RequestBondCoupons().
Parameters
- instruments: list of strings with tickers or FIGIs.
- xlsx: if True then also exports Pandas DataFrame to xlsx-file
bondsXLSXFile, defaultext-bonds.xlsx, for further used by data scientists or stock analytics.
Returns
wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. In XLSX-file and Pandas DataFrame fields mean: - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
4377 def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame: 4378 """ 4379 Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default. 4380 4381 WARNING! This is too long operation if a lot of bonds requested from broker server. 4382 4383 See also: `ShowBondsCalendar()`, `ExtendBondsData()`. 4384 4385 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4386 extended information about bonds: main info, current prices, bond payment calendar, 4387 coupon yields, current yields and some statistics etc. 4388 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4389 :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default, 4390 for further used by data scientists or stock analytics. 4391 :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon 4392 """ 4393 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4394 extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False) 4395 4396 uLogger.debug("Generating bond payments calendar data. Wait, please...") 4397 4398 colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"] 4399 colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"] 4400 calendar = None 4401 for bond in extBonds.iterrows(): 4402 for item in bond[1]["calendar"]: 4403 cData = { 4404 "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()), 4405 "couponDate": item["couponDate"], 4406 "figi": bond[1]["figi"], 4407 "ticker": bond[1]["ticker"], 4408 "name": bond[1]["name"], 4409 "couponNumber": item["couponNumber"], 4410 "payOneBond": item["payOneBond"], 4411 "payCurrency": item["payCurrency"], 4412 "couponType": item["couponType"], 4413 "couponPeriod": item["couponPeriod"], 4414 "fixDate": item["fixDate"], 4415 "couponStartDate": item["couponStartDate"], 4416 "couponEndDate": item["couponEndDate"], 4417 } 4418 4419 if calendar is None: 4420 calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID)) 4421 4422 else: 4423 calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True) 4424 4425 if calendar is not None: 4426 calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True) # sort all payments for all bonds by payment date 4427 4428 # Saving calendar from Pandas DataFrame to XLSX sheet: 4429 if xlsx: 4430 xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx" 4431 4432 with pd.ExcelWriter( 4433 path=xlsxCalendarFile, 4434 date_format=TKS_DATE_FORMAT, 4435 datetime_format=TKS_DATE_TIME_FORMAT, 4436 mode="w", 4437 ) as writer: 4438 humanReadable = calendar.copy(deep=True) 4439 humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0]) 4440 humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0]) 4441 humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0]) 4442 humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0]) 4443 humanReadable.columns = colNames # human-readable column names 4444 4445 humanReadable.to_excel( 4446 writer, 4447 sheet_name="Bond payments calendar", 4448 index=False, 4449 encoding="UTF-8", 4450 freeze_panes=(1, 2), 4451 ) # saving as XLSX-file with freeze first row and column as headers 4452 4453 del humanReadable # release df in memory 4454 4455 uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile))) 4456 4457 return calendar
Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, calendar.xlsx by default.
WARNING! This is too long operation if a lot of bonds requested from broker server.
See also: ShowBondsCalendar(), ExtendBondsData().
Parameters
- extBonds: Pandas DataFrame object returns by
ExtendBondsData()method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter isNonethen usedfigiortickeras bond name and then calculateExtendBondsData(). - xlsx: if True then also exports Pandas DataFrame to file
calendarFile+".xlsx",calendar.xlsxby default, for further used by data scientists or stock analytics.
Returns
Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
4459 def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True, onlyFiles=False) -> str: 4460 """ 4461 Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond. 4462 Also, creates Markdown file with calendar data, `calendar.md` by default. 4463 4464 See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`. 4465 4466 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4467 extended information about bonds: main info, current prices, bond payment calendar, 4468 coupon yields, current yields and some statistics etc. 4469 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4470 :param show: if `True` then also printing bonds payment calendar to the console, 4471 otherwise save to file `calendarFile` only. `False` by default. 4472 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 4473 :return: multilines text in Markdown format with bonds payment calendar as a table. 4474 """ 4475 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4476 extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=show or onlyFiles) 4477 4478 infoText = "# Bond payments calendar\n\n" 4479 4480 calendar = self.CreateBondsCalendar(extBonds, xlsx=show or onlyFiles) # generate Pandas DataFrame with full calendar data 4481 4482 if not (calendar is None or calendar.empty): 4483 splitLine = "| | | | | | | | | |\n" 4484 4485 info = [ 4486 "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4487 "| Paid | Payment date | FIGI | Ticker | No. | Value | Type | Period | End registry date |\n", 4488 "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n", 4489 ] 4490 4491 newMonth = False 4492 notOneBond = calendar["figi"].nunique() > 1 4493 for i, bond in enumerate(calendar.iterrows()): 4494 if newMonth and notOneBond: 4495 info.append(splitLine) 4496 4497 info.append( 4498 "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format( 4499 " √" if bond[1]["paid"] else " —", 4500 bond[1]["couponDate"].split("T")[0], 4501 bond[1]["figi"], 4502 bond[1]["ticker"], 4503 bond[1]["couponNumber"], 4504 "{} {}".format( 4505 "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."), 4506 bond[1]["payCurrency"], 4507 ), 4508 bond[1]["couponType"], 4509 bond[1]["couponPeriod"], 4510 bond[1]["fixDate"].split("T")[0], 4511 ) 4512 ) 4513 4514 if i < len(calendar.values) - 1: 4515 curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4516 nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4517 newMonth = False if curDate.month == nextDate.month else True 4518 4519 else: 4520 newMonth = False 4521 4522 infoText += "".join(info) 4523 4524 if show and not onlyFiles: 4525 uLogger.info("{}".format(infoText)) 4526 4527 if self.calendarFile is not None and (show or onlyFiles): 4528 with open(self.calendarFile, "w", encoding="UTF-8") as fH: 4529 fH.write(infoText) 4530 4531 uLogger.info("Bond payments calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile))) 4532 4533 if self.useHTMLReports: 4534 htmlFilePath = self.calendarFile.replace(".md", ".html") if self.calendarFile.endswith(".md") else self.calendarFile + ".html" 4535 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4536 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Bond payments calendar", commonCSS=COMMON_CSS, markdown=infoText)) 4537 4538 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4539 4540 else: 4541 infoText += "No data\n" 4542 4543 return infoText
Show bond payments calendar as a table. One row in input bonds dataframe contains one bond.
Also, creates Markdown file with calendar data, calendar.md by default.
See also: ShowInstrumentInfo(), RequestBondCoupons(), CreateBondsCalendar() and ExtendBondsData().
Parameters
- extBonds: Pandas DataFrame object returns by
ExtendBondsData()method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter isNonethen usedfigiortickeras bond name and then calculateExtendBondsData(). - show: if
Truethen also printing bonds payment calendar to the console, otherwise save to filecalendarFileonly.Falseby default. - onlyFiles: if
Truethen do not show Markdown table in the console, but only generates report files.
Returns
multilines text in Markdown format with bonds payment calendar as a table.
4545 def OverviewAccounts(self, show: bool = False, onlyFiles=False) -> dict: 4546 """ 4547 Method for parsing and show simple table with all available user accounts. 4548 4549 See also: `RequestAccounts()` and `OverviewUserInfo()` methods. 4550 4551 :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log. 4552 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 4553 :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict: 4554 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, 4555 "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", 4556 "status": "Opened and active account", "opened": "2018-05-23 00:00:00", 4557 "closed": "—", "access": "Full access" }, ...}}` 4558 """ 4559 rawAccounts = self.RequestAccounts() # Raw responses with accounts 4560 4561 # This is an array of dict with user accounts, its `accountId`s and some parsed data: 4562 accounts = { 4563 item["id"]: { 4564 "type": TKS_ACCOUNT_TYPES[item["type"]], 4565 "name": item["name"], 4566 "status": TKS_ACCOUNT_STATUSES[item["status"]], 4567 "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4568 "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—", 4569 "access": TKS_ACCESS_LEVELS[item["accessLevel"]], 4570 } for item in rawAccounts["accounts"] 4571 } 4572 4573 # Raw and parsed data with some fields replaced in "stat" section: 4574 view = { 4575 "rawAccounts": rawAccounts, 4576 "stat": accounts, 4577 } 4578 4579 # --- Prepare simple text table with only accounts data in human-readable format: 4580 if show or onlyFiles: 4581 info = [ 4582 "# User accounts\n\n", 4583 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4584 "| Account ID | Type | Status | Name |\n", 4585 "|--------------|---------------------------|---------------------------|--------------------------------|\n", 4586 ] 4587 4588 for account in view["stat"].keys(): 4589 info.extend([ 4590 "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format( 4591 account, 4592 view["stat"][account]["type"], 4593 view["stat"][account]["status"], 4594 view["stat"][account]["name"], 4595 ) 4596 ]) 4597 4598 infoText = "".join(info) 4599 4600 if show and not onlyFiles: 4601 uLogger.info(infoText) 4602 4603 if self.userAccountsFile and (show or onlyFiles): 4604 with open(self.userAccountsFile, "w", encoding="UTF-8") as fH: 4605 fH.write(infoText) 4606 4607 uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile))) 4608 4609 if self.useHTMLReports: 4610 htmlFilePath = self.userAccountsFile.replace(".md", ".html") if self.userAccountsFile.endswith(".md") else self.userAccountsFile + ".html" 4611 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4612 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User accounts", commonCSS=COMMON_CSS, markdown=infoText)) 4613 4614 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4615 4616 return view
Method for parsing and show simple table with all available user accounts.
See also: RequestAccounts() and OverviewUserInfo() methods.
Parameters
- show: if
Falsethen only dictionary with accounts data returns, ifTruethen also print it to log. - onlyFiles: if
Truethen do not show Markdown table in the console, but only generates report files.
Returns
dict with parsed accounts data received from
RequestAccounts()method. Example of dict:view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", "status": "Opened and active account", "opened": "2018-05-23 00:00:00", "closed": "—", "access": "Full access" }, ...}}
4618 def OverviewUserInfo(self, show: bool = False, onlyFiles=False) -> dict: 4619 """ 4620 Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). 4621 4622 See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods. 4623 4624 :param show: if `False` then only dictionary returns, if `True` then also print user's data to log. 4625 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 4626 :return: dict with raw parsed data from server and some calculated statistics about it. 4627 """ 4628 overview = self.Overview(show=False) # Request current user portfolio for the ability to calculate missing funds 4629 tmpTicker = self._ticker 4630 self._ticker = "RUB000UTSTOM" # This instrument show in rub how much money cost current margin 4631 missing = self.GetInstrumentFromPortfolio(portfolio=overview) 4632 self._ticker = tmpTicker 4633 4634 rawUserInfo = self.RequestUserInfo() # Raw response with common user info 4635 overviewAccount = self.OverviewAccounts(show=False) # Raw and parsed accounts data 4636 rawAccounts = overviewAccount["rawAccounts"] # Raw response with user accounts data 4637 accounts = overviewAccount["stat"] # Dict with only statistics about user accounts 4638 rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()} # Raw response with margin calculation for every account ID 4639 rawTariffLimits = self.RequestTariffLimits() # Raw response with limits of current tariff 4640 4641 # This is dict with parsed common user data: 4642 userInfo = { 4643 "premium": "Yes" if rawUserInfo["premStatus"] else "No", 4644 "qualified": "Yes" if rawUserInfo["qualStatus"] else "No", 4645 "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]], 4646 "tariff": rawUserInfo["tariff"], 4647 } 4648 4649 # This is an array of dict with parsed margin statuses for every account IDs: 4650 margins = {} 4651 for accountId in accounts.keys(): 4652 if rawMargins[accountId]: 4653 margins[accountId] = { 4654 "currency": rawMargins[accountId]["liquidPortfolio"]["currency"], 4655 "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]), 4656 "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]), 4657 "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]), 4658 "diff": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]), 4659 "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]), 4660 "missing": missing["volume"], 4661 } 4662 4663 else: 4664 margins[accountId] = {} # Server response: margin status is disabled for current accountId 4665 4666 unary = {} # unary-connection limits 4667 for item in rawTariffLimits["unaryLimits"]: 4668 if item["limitPerMinute"] in unary.keys(): 4669 unary[item["limitPerMinute"]].extend(item["methods"]) 4670 4671 else: 4672 unary[item["limitPerMinute"]] = item["methods"] 4673 4674 stream = {} # stream-connection limits 4675 for item in rawTariffLimits["streamLimits"]: 4676 if item["limit"] in stream.keys(): 4677 stream[item["limit"]].extend(item["streams"]) 4678 4679 else: 4680 stream[item["limit"]] = item["streams"] 4681 4682 # This is dict with parsed limits of current tariff (connections, API methods etc.): 4683 limits = { 4684 "unary": unary, 4685 "stream": stream, 4686 } 4687 4688 # Raw and parsed data as an output result: 4689 view = { 4690 "rawUserInfo": rawUserInfo, 4691 "rawAccounts": rawAccounts, 4692 "rawMargins": rawMargins, 4693 "rawTariffLimits": rawTariffLimits, 4694 "stat": { 4695 "overview": overview, 4696 "userInfo": userInfo, 4697 "accounts": accounts, 4698 "margins": margins, 4699 "limits": limits, 4700 }, 4701 } 4702 4703 # --- Prepare text table with user information in human-readable format: 4704 if show or onlyFiles: 4705 info = [ 4706 "# Full user information\n\n", 4707 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4708 "## Common information\n\n", 4709 "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]), 4710 "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]), 4711 "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]), 4712 "* **Allowed to work with instruments:**\n{}\n".format("".join([" - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])), 4713 "\n## User accounts\n\n", 4714 ] 4715 4716 for account in view["stat"]["accounts"].keys(): 4717 info.extend([ 4718 "### ID: [{}]\n\n".format(account), 4719 "| Parameters | Values |\n", 4720 "|----------------------|--------------------------------------------------------------|\n", 4721 "| Account type: | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]), 4722 "| Account name: | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]), 4723 "| Account status: | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]), 4724 "| Access level: | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]), 4725 "| Date opened: | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]), 4726 "| Date closed: | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]), 4727 ]) 4728 4729 if margins[account]: 4730 info.extend([ 4731 "| Margin status: | Enabled |\n", 4732 "| - Liquid portfolio: | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])), 4733 "| - Margin starting: | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])), 4734 "| - Margin minimum: | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])), 4735 "| - Margin difference: | {:<60} |\n".format("{} {}".format(margins[account]["diff"], margins[account]["currency"])), 4736 "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)), 4737 "| - Not covered funds: | {:<60} |\n\n".format("{:.2f} {}".format(margins[account]["missing"], margins[account]["currency"])), 4738 ]) 4739 4740 else: 4741 info.append("| Margin status: | Disabled |\n\n") 4742 4743 info.extend([ 4744 "\n## Current user tariff limits\n", 4745 "\n### See also\n", 4746 "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n", 4747 "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n", 4748 " - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n", 4749 " - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n", 4750 "\n### Unary limits\n", 4751 ]) 4752 4753 if unary: 4754 for key, values in sorted(unary.items()): 4755 info.append("\n* Max requests per minute: {}\n".format(key)) 4756 4757 for value in values: 4758 info.append(" - {}\n".format(value)) 4759 4760 else: 4761 info.append("\nNot available\n") 4762 4763 info.append("\n### Stream limits\n") 4764 4765 if stream: 4766 for key, values in sorted(stream.items()): 4767 info.append("\n* Max stream connections: {}\n".format(key)) 4768 4769 for value in values: 4770 info.append(" - {}\n".format(value)) 4771 4772 else: 4773 info.append("\nNot available\n") 4774 4775 infoText = "".join(info) 4776 4777 if show and not onlyFiles: 4778 uLogger.info(infoText) 4779 4780 if self.userInfoFile and (show or onlyFiles): 4781 with open(self.userInfoFile, "w", encoding="UTF-8") as fH: 4782 fH.write(infoText) 4783 4784 uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile))) 4785 4786 if self.useHTMLReports: 4787 htmlFilePath = self.userInfoFile.replace(".md", ".html") if self.userInfoFile.endswith(".md") else self.userInfoFile + ".html" 4788 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4789 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User info", commonCSS=COMMON_CSS, markdown=infoText)) 4790 4791 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4792 4793 return view
Method for parsing and show all available user's data (accountIds, common user information, margin status and tariff connections limit).
See also: OverviewAccounts(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits() methods.
Parameters
- show: if
Falsethen only dictionary returns, ifTruethen also print user's data to log. - onlyFiles: if
Truethen do not show Markdown table in the console, but only generates report files.
Returns
dict with raw parsed data from server and some calculated statistics about it.
4796class Args: 4797 """ 4798 If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object. 4799 """ 4800 def __init__(self, **kwargs): 4801 self.__dict__.update(kwargs) 4802 4803 def __getattr__(self, item): 4804 return None
If Main() function is imported as module, then this class used to convert arguments from **kwargs as object.
4807def ParseArgs(): 4808 """This function get and parse command line keys.""" 4809 parser = ArgumentParser() # command-line string parser 4810 4811 parser.description = "TKSBrokerAPI is a trading platform for automation on Python to simplify the implementation of trading scenarios and work with Tinkoff Invest API server via the REST protocol. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md" 4812 parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]" 4813 4814 # --- options: 4815 4816 parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the platform. `False` by default.") 4817 parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/") 4818 parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.") 4819 4820 parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.") 4821 parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).") 4822 4823 parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.") 4824 parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.") 4825 4826 parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.") 4827 parser.add_argument("--html", "--HTML", action="store_true", default=False, help="Option: if key present then TKSBrokerAPI generate also HTML reports from Markdown. False by default.") 4828 4829 parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.") 4830 parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.") 4831 parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.") 4832 4833 parser.add_argument("--debug-level", "--log-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.") 4834 parser.add_argument("--more", "--more-debug", action="store_true", default=False, help="Option: `--debug-level` key only switch log level verbosity, but in addition `--more` key enable all debug information, such as net request and response headers in all methods.") 4835 parser.add_argument("--tag", type=str, default="", help="Option: identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode. Default: `""` (empty string).") 4836 4837 # --- commands: 4838 4839 parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.") 4840 4841 parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.") 4842 parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.") 4843 parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4844 parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.") 4845 parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!") 4846 parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4847 parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!") 4848 parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.") 4849 4850 parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.") 4851 parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.") 4852 parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.") 4853 parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.") 4854 parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.") 4855 parser.add_argument("--overview-calendar", action="store_true", help="Action: shows only the bonds calendar section (if these present in portfolio). Also, you can define `--output` key to save this information to file, default: `overview-calendar.md`.") 4856 4857 parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.") 4858 parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.") 4859 parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.") 4860 parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).") 4861 4862 parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.") 4863 parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4864 parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4865 4866 parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.") 4867 parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!") 4868 parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!") 4869 parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4870 parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4871 # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4872 # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4873 4874 parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4875 parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4876 parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` (high priority) or `--figi` keys, including for currencies tickers.") 4877 parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers or FIGIs, including for currencies tickers or FIGIs.") 4878 parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations. If the `--close-all` key present with the `--ticker` or `--figi` keys, then positions and all open limit and stop orders for the specified instrument are closed.") 4879 4880 parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.") 4881 parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.") 4882 parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.") 4883 4884 cmdArgs = parser.parse_args() 4885 return cmdArgs
This function get and parse command line keys.
4888def Main(**kwargs): 4889 """ 4890 Main function for work with TKSBrokerAPI in the console. 4891 4892 See examples: 4893 - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md 4894 - in russian: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README.md 4895 """ 4896 args = Args(**kwargs) if kwargs else ParseArgs() # get and parse command-line parameters or use **kwarg parameters 4897 4898 if args.debug_level: 4899 uLogger.level = 10 # always debug level by default 4900 uLogger.handlers[0].level = args.debug_level # level for STDOUT 4901 4902 exitCode = 0 4903 start = datetime.now(tzutc()) 4904 uLogger.debug("=-" * 50) 4905 uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format( 4906 start.strftime(TKS_PRINT_DATE_TIME_FORMAT), 4907 start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4908 )) 4909 4910 # trying to calculate full current version: 4911 buildVersion = __version__ 4912 try: 4913 v = version("tksbrokerapi") 4914 buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0" # set version as major.minor.dev0 if run as local build or local script 4915 4916 except Exception: 4917 buildVersion = __version__ + ".dev0" # if an errors occurred then also set version as major.minor.dev0 4918 4919 uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion)) 4920 uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT)) 4921 4922 try: 4923 if args.version: 4924 print("TKSBrokerAPI {}".format(buildVersion)) 4925 uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion)) 4926 4927 else: 4928 # Init class for trading with Tinkoff Broker: 4929 trader = TinkoffBrokerServer( 4930 token=args.token, 4931 accountId=args.account_id, 4932 useCache=not args.no_cache, 4933 ) 4934 4935 if args.tag is not None: 4936 trader.tag = args.tag # Identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode 4937 4938 # --- set some options: 4939 4940 if args.more: 4941 trader.moreDebug = True 4942 uLogger.warning("More debug info mode is enabled! See network requests, responses and its headers in the full log or run TKSBrokerAPI platform with the `--verbosity 10` to show theres in console.") 4943 4944 if args.html: 4945 trader.useHTMLReports = True 4946 4947 if args.ticker: 4948 ticker = str(args.ticker).upper() # Tickers may be upper case only 4949 4950 if ticker in trader.aliasesKeys: 4951 trader.ticker = trader.aliases[ticker] # Replace some tickers with its aliases 4952 4953 else: 4954 trader.ticker = ticker 4955 4956 if args.figi: 4957 trader.figi = str(args.figi).upper() # FIGIs may be upper case only 4958 4959 if args.depth is not None: 4960 trader.depth = args.depth 4961 4962 # --- do one command: 4963 4964 if args.list: 4965 if args.output is not None: 4966 trader.instrumentsFile = args.output 4967 4968 trader.ShowInstrumentsInfo(show=True) 4969 4970 elif args.list_xlsx: 4971 trader.DumpInstrumentsAsXLSX(forceUpdate=False) 4972 4973 elif args.bonds_xlsx is not None: 4974 if args.output is not None: 4975 trader.bondsXLSXFile = args.output 4976 4977 if len(args.bonds_xlsx) == 0: 4978 trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=True) # request bonds with all available tickers 4979 4980 else: 4981 trader.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True) # request list of given bonds 4982 4983 elif args.search: 4984 if args.output is not None: 4985 trader.searchResultsFile = args.output 4986 4987 trader.SearchInstruments(pattern=args.search[0], show=True) 4988 4989 elif args.info: 4990 if not (args.ticker or args.figi): 4991 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4992 raise Exception("Ticker or FIGI required") 4993 4994 if args.output is not None: 4995 trader.infoFile = args.output 4996 4997 if args.ticker: 4998 trader.SearchByTicker(requestPrice=True, show=True) # show info and current prices by ticker name 4999 5000 else: 5001 trader.SearchByFIGI(requestPrice=True, show=True) # show info and current prices by FIGI id 5002 5003 elif args.calendar is not None: 5004 if args.output is not None: 5005 trader.calendarFile = args.output 5006 5007 if len(args.calendar) == 0: 5008 bondsData = trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=False) # request bonds with all available tickers 5009 5010 else: 5011 bondsData = trader.ExtendBondsData(instruments=args.calendar, xlsx=False) # request list of given bonds 5012 5013 trader.ShowBondsCalendar(extBonds=bondsData, show=True) # shows bonds payment calendar only 5014 5015 elif args.price: 5016 if not (args.ticker or args.figi): 5017 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 5018 raise Exception("Ticker or FIGI required") 5019 5020 trader.GetCurrentPrices(show=True) 5021 5022 elif args.prices is not None: 5023 if args.output is not None: 5024 trader.pricesFile = args.output 5025 5026 trader.GetListOfPrices(instruments=args.prices, show=True) # WARNING: too long wait for a lot of instruments prices 5027 5028 elif args.overview: 5029 if args.output is not None: 5030 trader.overviewFile = args.output 5031 5032 trader.Overview(show=True, details="full") 5033 5034 elif args.overview_digest: 5035 if args.output is not None: 5036 trader.overviewDigestFile = args.output 5037 5038 trader.Overview(show=True, details="digest") 5039 5040 elif args.overview_positions: 5041 if args.output is not None: 5042 trader.overviewPositionsFile = args.output 5043 5044 trader.Overview(show=True, details="positions") 5045 5046 elif args.overview_orders: 5047 if args.output is not None: 5048 trader.overviewOrdersFile = args.output 5049 5050 trader.Overview(show=True, details="orders") 5051 5052 elif args.overview_analytics: 5053 if args.output is not None: 5054 trader.overviewAnalyticsFile = args.output 5055 5056 trader.Overview(show=True, details="analytics") 5057 5058 elif args.overview_calendar: 5059 if args.output is not None: 5060 trader.overviewAnalyticsFile = args.output 5061 5062 trader.Overview(show=True, details="calendar") 5063 5064 elif args.deals is not None: 5065 if args.output is not None: 5066 trader.reportFile = args.output 5067 5068 if 0 <= len(args.deals) < 3: 5069 trader.Deals( 5070 start=args.deals[0] if len(args.deals) >= 1 else None, 5071 end=args.deals[1] if len(args.deals) == 2 else None, 5072 show=True, # Always show deals report in console 5073 showCancelled=not args.no_cancelled, # If --no-cancelled key then remove cancelled operations from the deals report. False by default. 5074 ) 5075 5076 else: 5077 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 5078 raise Exception("Incorrect value") 5079 5080 elif args.history is not None: 5081 if args.output is not None: 5082 trader.historyFile = args.output 5083 5084 if 0 <= len(args.history) < 3: 5085 dataReceived = trader.History( 5086 start=args.history[0] if len(args.history) >= 1 else None, 5087 end=args.history[1] if len(args.history) == 2 else None, 5088 interval="hour" if args.interval is None or not args.interval else args.interval, 5089 onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing, 5090 csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep, 5091 show=True, # shows all downloaded candles in console 5092 ) 5093 5094 if args.render_chart is not None and dataReceived is not None: 5095 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 5096 5097 trader.ShowHistoryChart( 5098 candles=dataReceived, 5099 interact=iChart, 5100 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 5101 ) 5102 5103 else: 5104 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 5105 raise Exception("Incorrect value") 5106 5107 elif args.load_history is not None: 5108 histData = trader.LoadHistory(filePath=args.load_history) # load data from file and show history in console 5109 5110 if args.render_chart is not None and histData is not None: 5111 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 5112 trader.ticker = os.path.basename(args.load_history) # use filename as ticker name for PriceGenerator's chart 5113 5114 trader.ShowHistoryChart( 5115 candles=histData, 5116 interact=iChart, 5117 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 5118 ) 5119 5120 elif args.trade is not None: 5121 if 1 <= len(args.trade) <= 5: 5122 trader.Trade( 5123 operation=args.trade[0], 5124 lots=int(args.trade[1]) if len(args.trade) >= 2 else 1, 5125 tp=float(args.trade[2]) if len(args.trade) >= 3 else 0., 5126 sl=float(args.trade[3]) if len(args.trade) >= 4 else 0., 5127 expDate=args.trade[4] if len(args.trade) == 5 else "Undefined", 5128 ) 5129 5130 else: 5131 uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 5132 5133 elif args.buy is not None: 5134 if 0 <= len(args.buy) <= 4: 5135 trader.Buy( 5136 lots=int(args.buy[0]) if len(args.buy) >= 1 else 1, 5137 tp=float(args.buy[1]) if len(args.buy) >= 2 else 0., 5138 sl=float(args.buy[2]) if len(args.buy) >= 3 else 0., 5139 expDate=args.buy[3] if len(args.buy) == 4 else "Undefined", 5140 ) 5141 5142 else: 5143 uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 5144 5145 elif args.sell is not None: 5146 if 0 <= len(args.sell) <= 4: 5147 trader.Sell( 5148 lots=int(args.sell[0]) if len(args.sell) >= 1 else 1, 5149 tp=float(args.sell[1]) if len(args.sell) >= 2 else 0., 5150 sl=float(args.sell[2]) if len(args.sell) >= 3 else 0., 5151 expDate=args.sell[3] if len(args.sell) == 4 else "Undefined", 5152 ) 5153 5154 else: 5155 uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 5156 5157 elif args.order: 5158 if 4 <= len(args.order) <= 7: 5159 trader.Order( 5160 operation=args.order[0], 5161 orderType=args.order[1], 5162 lots=int(args.order[2]), 5163 targetPrice=float(args.order[3]), 5164 limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0., 5165 stopType=args.order[5] if len(args.order) >= 6 else "Limit", 5166 expDate=args.order[6] if len(args.order) == 7 else "Undefined", 5167 ) 5168 5169 else: 5170 uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`") 5171 5172 elif args.buy_limit: 5173 trader.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1]) 5174 5175 elif args.sell_limit: 5176 trader.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1]) 5177 5178 elif args.buy_stop: 5179 if 2 <= len(args.buy_stop) <= 7: 5180 trader.BuyStop( 5181 lots=int(args.buy_stop[0]), 5182 targetPrice=float(args.buy_stop[1]), 5183 limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0., 5184 stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit", 5185 expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined", 5186 ) 5187 5188 else: 5189 uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 5190 5191 elif args.sell_stop: 5192 if 2 <= len(args.sell_stop) <= 7: 5193 trader.SellStop( 5194 lots=int(args.sell_stop[0]), 5195 targetPrice=float(args.sell_stop[1]), 5196 limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0., 5197 stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit", 5198 expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined", 5199 ) 5200 5201 else: 5202 uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help") 5203 5204 # elif args.buy_order_grid is not None: 5205 # # update order grid work with api v2 5206 # if len(args.buy_order_grid) == 2: 5207 # orderParams = trader.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid)) 5208 # 5209 # for order in orderParams: 5210 # trader.Order(operation="Buy", lots=order["lot"], price=order["price"]) 5211 # 5212 # else: 5213 # uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 5214 # 5215 # elif args.sell_order_grid is not None: 5216 # # update order grid work with api v2 5217 # if len(args.sell_order_grid) >= 2: 5218 # orderParams = trader.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid)) 5219 # 5220 # for order in orderParams: 5221 # trader.Order(operation="Sell", lots=order["lot"], price=order["price"]) 5222 # 5223 # else: 5224 # uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 5225 5226 elif args.close_order is not None: 5227 trader.CloseOrders(args.close_order) # close only one order 5228 5229 elif args.close_orders is not None: 5230 trader.CloseOrders(args.close_orders) # close list of orders 5231 5232 elif args.close_trade: 5233 if not (args.ticker or args.figi): 5234 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 5235 raise Exception("Ticker or FIGI required") 5236 5237 if args.ticker: 5238 trader.CloseTrades([str(args.ticker).upper()]) # close only one trade by ticker (priority) 5239 5240 else: 5241 trader.CloseTrades([str(args.figi).upper()]) # close only one trade by FIGI 5242 5243 elif args.close_trades is not None: 5244 trader.CloseTrades(args.close_trades) # close trades for list of tickers 5245 5246 elif args.close_all is not None: 5247 if args.ticker: 5248 trader.CloseAllByTicker(instrument=str(args.ticker).upper()) 5249 5250 elif args.figi: 5251 trader.CloseAllByFIGI(instrument=str(args.figi).upper()) 5252 5253 else: 5254 trader.CloseAll(*args.close_all) 5255 5256 elif args.limits: 5257 if args.output is not None: 5258 trader.withdrawalLimitsFile = args.output 5259 5260 trader.OverviewLimits(show=True) 5261 5262 elif args.user_info: 5263 if args.output is not None: 5264 trader.userInfoFile = args.output 5265 5266 trader.OverviewUserInfo(show=True) 5267 5268 elif args.account: 5269 if args.output is not None: 5270 trader.userAccountsFile = args.output 5271 5272 trader.OverviewAccounts(show=True) 5273 5274 else: 5275 uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.") 5276 raise Exception("There is no command to execute") 5277 5278 except Exception: 5279 trace = tb.format_exc() 5280 for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]: 5281 if e in trace: 5282 uLogger.error("Check your Internet connection! Failed to establish connection to broker server!") 5283 break 5284 5285 uLogger.debug(trace) 5286 uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues") 5287 exitCode = 255 # an error occurred, must be open a ticket for this issue 5288 5289 finally: 5290 finish = datetime.now(tzutc()) 5291 5292 if exitCode == 0: 5293 if args.more: 5294 uLogger.debug("All operations were finished success (summary code is 0).") 5295 5296 else: 5297 uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format( 5298 os.path.abspath(uLog.defaultLogFile), exitCode, 5299 )) 5300 5301 uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start)) 5302 uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format( 5303 finish.strftime(TKS_PRINT_DATE_TIME_FORMAT), 5304 finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 5305 )) 5306 uLogger.debug("=-" * 50) 5307 5308 if not kwargs: 5309 sys.exit(exitCode) 5310 5311 else: 5312 return exitCode
Main function for work with TKSBrokerAPI in the console.
See examples:
